├── .dockerignore ├── .env.development ├── .env.production ├── .envrc ├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── Dockerfile.debian ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── coming-soon.spec.ts │ └── dashboard.spec.ts ├── plugins │ └── index.js ├── support │ └── index.ts └── tsconfig.json ├── flake.lock ├── flake.nix ├── next-env.d.ts ├── next.config.js ├── package.json ├── public ├── favicon.ico ├── landing-vector.svg ├── nav-vector.svg └── vercel.svg ├── src ├── components │ ├── Addon.tsx │ ├── App.tsx │ ├── AppCreateModal.tsx │ ├── Chakra.tsx │ ├── ColorButton.spec.tsx │ ├── ColorButton.tsx │ ├── ConfirmDelete.spec.tsx │ ├── ConfirmDelete.tsx │ ├── Domain.tsx │ ├── KVEntry.tsx │ ├── Logs.tsx │ ├── Stat.tsx │ ├── TeamCreateModal.tsx │ ├── Toast.tsx │ └── forms │ │ ├── team-create.tsx │ │ └── team-join.tsx ├── layouts │ ├── AppLayout.tsx │ ├── DashboardLayout.tsx │ ├── HaasLayout.tsx │ └── TeamLayout.tsx ├── lib │ ├── dummyData.ts │ ├── failBuildEventLogs.json │ ├── fetch.ts │ ├── successBuildEventLogs.json │ ├── testHelpers.tsx │ └── withAuth.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _error.tsx │ ├── apps │ │ └── [id] │ │ │ ├── addons.tsx │ │ │ ├── deploy.tsx │ │ │ ├── domains.tsx │ │ │ ├── environment.tsx │ │ │ └── index.tsx │ ├── auth │ │ └── device.tsx │ ├── beep.tsx │ ├── builds │ │ └── [id].tsx │ ├── dashboard.tsx │ ├── index.tsx │ ├── landing.tsx │ ├── settings.tsx │ └── teams │ │ └── [id] │ │ ├── index.tsx │ │ ├── settings.tsx │ │ └── users.tsx ├── styles │ ├── globals.css │ └── nprogress.css ├── theme.ts └── types │ ├── build.ts │ ├── glyph.ts │ └── haas.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_BASE=http://localhost:3000/api 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_BASE=https://haas.hackclub.com/api 2 | NO_PROXY=true 3 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /cypress/ 2 | *.spec.* 3 | *.spec.* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "plugin:@typescript-eslint/recommended"], 3 | "rules": { 4 | "react/display-name": "off", 5 | "react-hooks/exhaustive-deps": "off", 6 | "@typescript-eslint/explicit-module-boundary-types": "off", 7 | "@typescript-eslint/no-empty-function": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - testing-environment 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | - run: yarn install 17 | - run: yarn build 18 | 19 | format: 20 | name: Format 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v2 25 | - run: yarn install 26 | - run: yarn format:check 27 | 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions/setup-node@v2 34 | - run: yarn install 35 | - run: yarn lint 36 | 37 | lint-docker: 38 | name: Lint (Dockerfile) 39 | container: hadolint/hadolint:latest-debian 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - run: hadolint Dockerfile 44 | 45 | test: 46 | name: Test 47 | if: "false" 48 | runs-on: ubuntu-latest 49 | services: 50 | postgres: 51 | image: postgres:13.4 52 | ports: 53 | - 5432:5432 54 | env: 55 | POSTGRES_PASSWORD: postgres 56 | # Set health checks to wait until postgres has started 57 | options: >- 58 | --health-cmd pg_isready 59 | --health-interval 10s 60 | --health-timeout 5s 61 | --health-retries 5 62 | api: 63 | image: ghcr.io/hack-as-a-service/api:main 64 | ports: 65 | - 5000:5000 66 | env: 67 | ROCKET_DATABASES: '{db = {url = "postgres://postgres:postgres@postgres:5432/postgres"}}' 68 | 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v2 72 | - name: Set up development database 73 | run: "docker run --network host --rm -e 'DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres' ghcr.io/hack-as-a-service/migrator:main" 74 | - name: Run Cypress E2E tests 75 | uses: cypress-io/github-action@v2 76 | with: 77 | build: yarn build 78 | start: yarn start 79 | browser: chrome 80 | config: defaultCommandTimeout=7000 81 | env: 82 | NO_PROXY: "false" 83 | NEXT_PUBLIC_API_BASE: http://localhost:3000/api 84 | - name: Run Cypress Component tests 85 | uses: cypress-io/github-action@v2 86 | with: 87 | install: false 88 | command: yarn cypress run-ct --browser=chrome --config defaultCommandTimeout=7000 89 | env: 90 | NO_PROXY: "false" 91 | - uses: actions/upload-artifact@v2 92 | if: ${{ failure() }} 93 | with: 94 | path: cypress/videos/*.mp4 95 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | frontend: 8 | needs: docker 9 | name: Frontend 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - run: mkdir -p ~/.ssh && echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa 14 | name: Install SSH key 15 | - run: echo "167.99.113.134 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIV4nOfENhtxnlRPutfcJQhuBaiXYCaU6e93BnI8y0WVEPXLirzgHAujtT0TZ6HAgIXvj+ZMqbJRZSKoN5wCuDk=" > ~/.ssh/known_hosts 16 | name: Trust SSH host 17 | - run: "echo 'deploy --image ghcr.io/hack-as-a-service/frontend:main --name haas_frontend' | ssh -T -i ~/.ssh/id_rsa deploy@167.99.113.134" 18 | name: Deploy frontend 19 | docker: 20 | name: Docker 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | packages: write 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Log in to the Container registry 29 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Extract metadata (tags, labels) for Docker 36 | id: meta 37 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 38 | with: 39 | images: ghcr.io/hack-as-a-service/frontend 40 | 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 43 | with: 44 | context: . 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | tests/unit/coverage 10 | .nyc_output 11 | 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | /cypress/videos 39 | /cypress/screenshots 40 | 41 | # direnv 42 | .direnv 43 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | out/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:16-alpine3.14 AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | # hadolint ignore=DL3018 5 | RUN apk add --no-cache libc6-compat 6 | WORKDIR /app 7 | COPY package.json yarn.lock ./ 8 | RUN yarn install --frozen-lockfile && yarn cache clean 9 | 10 | # Rebuild the source code only when needed 11 | FROM node:16-alpine3.14 AS builder 12 | WORKDIR /app 13 | COPY . . 14 | COPY --from=deps /app/node_modules ./node_modules 15 | RUN yarn build && yarn install --production --ignore-scripts --prefer-offline && yarn cache clean 16 | 17 | # Production image, copy all the files and run next 18 | FROM node:16-alpine3.14 AS runner 19 | WORKDIR /app 20 | 21 | ENV NODE_ENV production 22 | 23 | RUN addgroup -g 1001 -S nodejs && \ 24 | adduser -S nextjs -u 1001 25 | 26 | # You only need to copy next.config.js if you are NOT using the default configuration 27 | COPY --from=builder /app/next.config.js ./ 28 | COPY --from=builder /app/public ./public 29 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 30 | COPY --from=builder /app/node_modules ./node_modules 31 | COPY --from=builder /app/package.json ./package.json 32 | 33 | USER nextjs 34 | 35 | EXPOSE 3000 36 | 37 | # Next.js collects completely anonymous telemetry data about general usage. 38 | # Learn more here: https://nextjs.org/telemetry 39 | # Uncomment the following line in case you want to disable telemetry. 40 | # ENV NEXT_TELEMETRY_DISABLED 1 41 | 42 | CMD ["yarn", "start"] 43 | -------------------------------------------------------------------------------- /Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN yarn install && yarn cache clean && \ 8 | yarn build 9 | 10 | ENV NODE_ENV production 11 | 12 | EXPOSE 3000 13 | 14 | CMD ["yarn", "start"] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

Hack as a Service

5 | 6 |

📦 The app platform for @hackclub students.

7 | 8 |

9 | 10 |
11 | 12 | [![Contributors][contributors-shield]][contributors-url] 13 | [![Forks][forks-shield]][forks-url] 14 | [![Stargazers][stars-shield]][stars-url] 15 | [![Issues][issues-shield]][issues-url] 16 | [![License][license-shield]][license-url] 17 | 18 |
19 | 20 |

21 | Hack as a Service (or HaaS) is Hack Club's own PaaS (Platform as a Service): A scalable, simple, and inexpensive way for Hack Clubbers to host backend services (including web apps, games, Slack bots, and more). 22 |

23 | 24 | ## Documentation 25 | 26 | For more concise details on how to use Hack as a Service, check out our [documentation](https://haas.hackclub.com/docs/). 27 | 28 | ## Contributing 29 | 30 | How can I contribute? 31 | 32 | Before contributing to the frontend, you must go and setup the [API](https://github.com/hack-as-a-service/api) for Hack as a Service. 33 | 34 | After that follow the steps below -> 35 | 36 | 1. Clone the repo. 37 | 38 | ```sh 39 | git clone https://github.com/hack-as-a-service/frontend 40 | ``` 41 | 42 | 2. Change directory. 43 | 44 | ```sh 45 | cd frontend 46 | ``` 47 | 48 | 3. Install dependencies. 49 | 50 | ```sh 51 | yarn install 52 | ``` 53 | 54 | 4. Start the server. 55 | 56 | ```sh 57 | yarn dev 58 | ``` 59 | 60 | Once you have completed all these steps, you should be good to go 🚀. 61 | You can make your desired changes, and create a pull request once you are done. 62 | 63 | [contributors-shield]: https://img.shields.io/github/contributors/hack-as-a-service/frontend.svg?style=for-the-badge 64 | [contributors-url]: https://github.com/hack-as-a-service/frontend/graphs/contributors 65 | [forks-shield]: https://img.shields.io/github/forks/hack-as-a-service/frontend.svg?style=for-the-badge 66 | [forks-url]: https://github.com/hack-as-a-service/frontend/network/members 67 | [stars-shield]: https://img.shields.io/github/stars/hack-as-a-service/frontend.svg?style=for-the-badge 68 | [stars-url]: https://github.com/hack-as-a-service/frontend/stargazers 69 | [issues-shield]: https://img.shields.io/github/issues/hack-as-a-service/frontend.svg?style=for-the-badge 70 | [issues-url]: https://github.com/hack-as-a-service/frontend/issues 71 | [license-shield]: https://img.shields.io/github/license/hack-as-a-service/frontend?color=red&style=for-the-badge 72 | [license-url]: https://github.com/hack-as-a-service/frontend/blob/main/LICENSE 73 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "supportFile": "cypress/support/index.ts", 4 | "nodeVersion": "system", 5 | "viewportWidth": 1920, 6 | "viewportHeight": 1080, 7 | "experimentalSourceRewriting": true, 8 | "experimentalFetchPolyfill": true, 9 | "component": { 10 | "testFiles": "**/*.spec.{js,ts,jsx,tsx}", 11 | "componentFolder": "src/components" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/coming-soon.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Coming Soon page", () => { 4 | it("redirects to dashboard", () => { 5 | cy.login(); 6 | 7 | cy.visit("/"); 8 | cy.url().should("eq", "http://localhost:3000/dashboard"); 9 | }); 10 | 11 | it("tells the user how to log in", () => { 12 | cy.logout(); 13 | 14 | cy.get("p").contains("Log in"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/integration/dashboard.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Dashboard", () => { 4 | beforeEach(() => { 5 | cy.login(); 6 | }); 7 | 8 | it("can create an app", () => { 9 | const random = Cypress._.random(0, 1e6); 10 | const appName = `test-app-${random}`; 11 | 12 | cy.getCy("app-create-modal").should("not.exist"); 13 | 14 | cy.getCy("create-app").click(); 15 | 16 | cy.getCy("app-create-modal").should("exist"); 17 | 18 | cy.getCy("create-app-modal-slug").type(appName); 19 | cy.getCy("create-app-modal-submit").click(); 20 | 21 | cy.location("pathname").should("eq", `/apps/${appName}/deploy`); 22 | 23 | cy.go("back"); 24 | 25 | cy.getCy("personal-apps").contains(appName); 26 | }); 27 | 28 | it("can create a team without a display name", () => { 29 | const random = Cypress._.random(0, 1e6); 30 | const teamName = `test-team-no-display-${random}`; 31 | 32 | cy.getCy("team-create-modal").should("not.exist"); 33 | 34 | cy.getCy("create-team").click(); 35 | 36 | cy.getCy("team-create-modal").should("exist"); 37 | 38 | cy.getCy("create-team-modal-slug").type(teamName); 39 | 40 | cy.getCy("create-team-modal-submit").click(); 41 | 42 | cy.location("pathname").should("eq", `/teams/${teamName}`); 43 | 44 | cy.go("back"); 45 | 46 | cy.getCy("sidebar").contains(teamName); 47 | }); 48 | 49 | it("can create a team with a display name", () => { 50 | const random = Cypress._.random(0, 1e6); 51 | const teamName = `test-team-${random}`; 52 | 53 | cy.getCy("team-create-modal").should("not.exist"); 54 | 55 | cy.getCy("create-team").click(); 56 | 57 | cy.getCy("team-create-modal").should("exist"); 58 | 59 | cy.getCy("create-team-modal-slug").type(teamName); 60 | cy.getCy("create-team-modal-name").type("Test Team"); 61 | 62 | cy.getCy("create-team-modal-submit").click(); 63 | 64 | cy.location("pathname").should("eq", `/teams/${teamName}`); 65 | 66 | cy.go("back"); 67 | 68 | cy.getCy("sidebar").contains("Test Team"); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (on, config) => { 2 | if (config.testingType === "component") { 3 | require("@cypress/react/plugins/next")(on, config); 4 | } 5 | return config; 6 | }; 7 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import "cypress-react-selector"; 3 | 4 | declare global { 5 | namespace Cypress { 6 | interface Chainable { 7 | login(): void; 8 | logout(): void; 9 | getCy(id: string): Chainable; 10 | } 11 | } 12 | } 13 | 14 | Cypress.Commands.add("login", () => { 15 | cy.visit("/api/dev/login"); 16 | 17 | cy.url().should("eq", "http://localhost:3000/dashboard"); 18 | 19 | cy.getCookie("haas_token").should("exist"); 20 | }); 21 | 22 | Cypress.Commands.add("logout", () => { 23 | cy.visit("/api/logout"); 24 | 25 | cy.url().should("eq", "http://localhost:3000/"); 26 | 27 | cy.getCookie("haas_token").should("not.exist"); 28 | }); 29 | 30 | Cypress.Commands.add("getCy", (id: string) => { 31 | return cy.get(`[data-cy="${id}"]`); 32 | }); 33 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "cypress-react-selector"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1631561581, 6 | "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1632650849, 21 | "narHash": "sha256-PYKC/aP409p4+Gnn0Ix7gFJncg1NwQgguwdTEyC8t+A=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "b5182c214fac1e6db9f28ed8a7cfc2d0c255c763", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "ref": "nixpkgs-unstable", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A very basic flake"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let 9 | pkgs = nixpkgs.legacyPackages.${system}; 10 | in { 11 | devShell = pkgs.mkShell { 12 | nativeBuildInputs = [ 13 | pkgs.yarn 14 | pkgs.nodePackages.typescript-language-server 15 | ]; 16 | }; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('next').NextConfig} 3 | */ 4 | module.exports = { 5 | async rewrites() { 6 | if (process.env.NO_PROXY === "true") return []; 7 | 8 | return [ 9 | { 10 | source: "/api/:path*", 11 | destination: "http://localhost:5000/api/:path*", 12 | }, 13 | ]; 14 | }, 15 | async redirects() { 16 | return [ 17 | { 18 | source: "/apps/:slug/logs", 19 | destination: "/apps/:slug", 20 | permanent: false, 21 | }, 22 | ]; 23 | }, 24 | eslint: { 25 | dirs: ["src/pages/", "src/components/", "src/lib/", "src/layouts/"], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "format:check": "prettier -c .", 10 | "format:write": "prettier -w .", 11 | "lint": "next lint", 12 | "cypress:open": "cypress open" 13 | }, 14 | "dependencies": { 15 | "@chakra-ui/react": "^2.3.2", 16 | "@emotion/react": "^11.10.4", 17 | "@emotion/styled": "^11.10.4", 18 | "@hackclub/icons": "^0.0.9", 19 | "@hackclub/theme": "^0.3.3", 20 | "ansi-to-react": "^6.1.6", 21 | "core-js": "^3", 22 | "deepmerge-ts": "^2.0.1", 23 | "formik": "^2.2.9", 24 | "framer-motion": "^7.3.2", 25 | "nanoid": "^4.0.0", 26 | "next": "12", 27 | "nprogress": "^0.2.0", 28 | "prettier": "^2.7.1", 29 | "pretty-bytes": "^5.6.0", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-feather": "^2.0.9", 33 | "swr": "^1.1.2" 34 | }, 35 | "devDependencies": { 36 | "@cypress/react": "^5.11.0", 37 | "@cypress/webpack-dev-server": "^1.7.0", 38 | "@types/node": "^17.0.5", 39 | "@types/nprogress": "^0.2.0", 40 | "@types/react": "^17.0.37", 41 | "@typescript-eslint/eslint-plugin": "^4.33.0", 42 | "@typescript-eslint/parser": "^4.33.0", 43 | "cypress": "^9.1.0", 44 | "cypress-react-selector": "^2.3.13", 45 | "eslint": "^7.32.0", 46 | "eslint-config-next": "^12.0.7", 47 | "html-webpack-plugin": "^5.5.0", 48 | "typescript": "^4.5.2", 49 | "webpack": "^5.65.0", 50 | "webpack-dev-server": "4" 51 | }, 52 | "resolutions": { 53 | "source-map-resolve": "^0.6.0", 54 | "core-js": "^3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hack-as-a-service/frontend/aca945f158d60b543c3e9dcf94dda8f417ac3975/public/favicon.ico -------------------------------------------------------------------------------- /public/landing-vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/nav-vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Addon.tsx: -------------------------------------------------------------------------------- 1 | import { KVEntry } from "./KVEntry"; 2 | import { ConfirmDelete } from "./ConfirmDelete"; 3 | import { Stat } from "./Stat"; 4 | import { 5 | Flex, 6 | Button, 7 | Text, 8 | Heading, 9 | Img, 10 | Modal, 11 | ModalOverlay, 12 | ModalContent, 13 | ModalHeader, 14 | ModalFooter, 15 | ModalBody, 16 | ModalCloseButton, 17 | useDisclosure, 18 | useColorModeValue, 19 | } from "@chakra-ui/react"; 20 | 21 | import { useState } from "react"; 22 | import { IAddon, KVConfig } from "../types/haas"; 23 | import { devAddons, devAddonsOriginal } from "../lib/dummyData"; 24 | 25 | export function Addon({ 26 | name, 27 | activated: a, 28 | description, 29 | id, 30 | img, 31 | storage, 32 | config: c, 33 | price, 34 | }: IAddon) { 35 | const border = useColorModeValue("#00000033", "#ffffff33"); 36 | const secondary = useColorModeValue("gray.500", "gray.600"); 37 | const [activated, updateActive] = useState(a); 38 | const [newConfig, updateConfig] = useState(c); 39 | const { 40 | isOpen: manageIsOpen, 41 | onOpen: manageOnOpen, 42 | onClose: manageOnClose, 43 | } = useDisclosure(); 44 | const { 45 | isOpen: enableIsOpen, 46 | onOpen: enableOnOpen, 47 | onClose: enableOnClose, 48 | } = useDisclosure(); 49 | const { 50 | isOpen: confirmIsOpen, 51 | onOpen: confirmOnOpen, 52 | onClose: confirmOnClose, 53 | } = useDisclosure(); 54 | 55 | const [verb, setVerb] = useState("disable"); 56 | 57 | function closeAndDiscard() { 58 | manageOnClose(); 59 | devAddons[id] = devAddonsOriginal[id]; 60 | updateConfig(c); 61 | } 62 | 63 | return ( 64 | <> 65 | 73 | 74 | 75 | 76 | {name} 77 | 78 | {description} 79 | 80 | 81 | 82 | 89 | {price} 90 | 91 | 92 | hn/gb/month 93 | 94 | 95 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 110 | Addons → {name} 111 | 116 | 117 | 118 | 119 | 120 | Any unsaved changes will be discarded. 121 | 122 | {Object.entries(newConfig).map((entry) => { 123 | const kv_id = entry[0]; 124 | const v = entry[1]; 125 | const obj = {}; 126 | obj[kv_id] = v; 127 | return ( 128 | { 132 | updateConfig({ ...newConfig, ...entry }); 133 | }} 134 | entry={obj} 135 | /> 136 | ); 137 | })} 138 | 139 | 140 | 141 | 155 | 169 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 195 | Enable {name}? 196 | 201 | 202 | 203 | You will be billed for all resources used by this addon. 204 | 205 | 206 | 207 | 230 | 231 | 232 | 233 | 234 | { 242 | // const obj = { 243 | // name, 244 | // activated, 245 | // id, 246 | // config: c, 247 | // img, 248 | // storage, 249 | // description, 250 | // }; 251 | 252 | const idx = devAddons.findIndex((o) => o.id == id); 253 | if (verb != "wipe") { 254 | devAddons[idx].activated = false; 255 | updateActive(false); 256 | } 257 | }} 258 | /> 259 | 260 | ); 261 | } 262 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { LinkBox, Flex, Heading, useColorMode, Box } from "@chakra-ui/react"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | 5 | export default function App({ 6 | name, 7 | url, 8 | enabled, 9 | }: { 10 | name: string; 11 | url: string; 12 | enabled: boolean; 13 | }) { 14 | const { colorMode } = useColorMode(); 15 | 16 | return ( 17 | 18 | 19 | 28 | 36 | 37 | 38 | {name} 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/AppCreateModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalHeader, 6 | ModalFooter, 7 | ModalBody, 8 | ModalCloseButton, 9 | Heading, 10 | Button, 11 | FormControl, 12 | FormLabel, 13 | Input, 14 | FormErrorMessage, 15 | InputGroup, 16 | InputRightAddon, 17 | } from "@chakra-ui/react"; 18 | 19 | import { Formik, FormikHelpers } from "formik"; 20 | import React, { useRef } from "react"; 21 | 22 | type Values = { slug: string }; 23 | 24 | export default function AppCreateModal({ 25 | isOpen, 26 | onClose, 27 | onSubmit, 28 | }: { 29 | isOpen: boolean; 30 | onClose: () => void; 31 | onSubmit: ( 32 | values: Values, 33 | formikHelpers: FormikHelpers 34 | ) => void | Promise; 35 | }) { 36 | const initialRef = useRef(); 37 | 38 | return ( 39 | 46 | 47 | { 51 | const errors: Partial = {}; 52 | if (!values.slug) { 53 | errors.slug = "This field is required."; 54 | } else if (!/^[a-z0-9\-]*$/.test(values.slug)) { 55 | errors.slug = 56 | "Your app name may only contain lowercase letters, numbers, and dashes."; 57 | } 58 | 59 | return errors; 60 | }} 61 | > 62 | {({ 63 | handleChange, 64 | handleBlur, 65 | values, 66 | handleSubmit, 67 | errors, 68 | isSubmitting, 69 | }) => ( 70 |
71 | 72 | 73 | Create An App 74 | 75 | 76 | 77 | 78 | Name 79 | 80 | 90 | .hackclub.app 91 | 92 | {errors.slug} 93 | 94 | 95 | 96 | 97 | 100 | 109 | 110 | 111 |
112 | )} 113 |
114 |
115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/components/Chakra.tsx: -------------------------------------------------------------------------------- 1 | import theme from "../theme"; 2 | import { deepmerge } from "deepmerge-ts"; 3 | import { GetServerSideProps } from "next"; 4 | 5 | import { 6 | ChakraProvider, 7 | cookieStorageManagerSSR, 8 | localStorageManager, 9 | } from "@chakra-ui/react"; 10 | 11 | export function Chakra({ cookies, children }) { 12 | const colorModeManager = 13 | typeof cookies === "string" 14 | ? cookieStorageManagerSSR(cookies) 15 | : localStorageManager; 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | 24 | export function withCookies(next: GetServerSideProps): GetServerSideProps { 25 | return async (ctx) => { 26 | return deepmerge(await next(ctx), { 27 | props: { 28 | cookies: ctx.req.headers.cookie ?? "", 29 | }, 30 | }); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/ColorButton.spec.tsx: -------------------------------------------------------------------------------- 1 | import { useColorMode, ColorMode } from "@chakra-ui/react"; 2 | import { mountChakra } from "../lib/testHelpers"; 3 | import ColorButton from "./ColorButton"; 4 | 5 | beforeEach(() => { 6 | // Chakra stores color mode in local storage 7 | cy.clearLocalStorage(); 8 | // Workaround for cypress-react-selector, makes react selector work between component tests 9 | global.isReactLoaded = false; 10 | }); 11 | 12 | it("has an aria-label", () => { 13 | mountChakra(); 14 | cy.waitForReact(); 15 | cy.react("ColorButton").should("have.attr", "aria-label"); 16 | }); 17 | 18 | it("changes color mode", () => { 19 | function ActualColorMode(_: { colorMode: ColorMode }) { 20 | return <>; 21 | } 22 | function GetColorMode() { 23 | const { colorMode } = useColorMode(); 24 | return ; 25 | } 26 | mountChakra( 27 | <> 28 | 29 | 30 | 31 | ); 32 | cy.waitForReact(); 33 | cy.getReact("ActualColorMode").getProps("colorMode").should("equal", "light"); 34 | cy.react("ColorButton").click(); 35 | cy.getReact("ActualColorMode").getProps("colorMode").should("equal", "dark"); 36 | cy.react("ColorButton").click(); 37 | cy.getReact("ActualColorMode").getProps("colorMode").should("equal", "light"); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/ColorButton.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "react-feather"; 2 | import { IconButton, useColorMode } from "@chakra-ui/react"; 3 | 4 | function ColorButton({ ...props }) { 5 | const { colorMode, toggleColorMode } = useColorMode(); 6 | 7 | const icon = colorMode === "dark" ? : ; 8 | 9 | return ( 10 | 18 | {icon} 19 | 20 | ); 21 | } 22 | 23 | export default ColorButton; 24 | -------------------------------------------------------------------------------- /src/components/ConfirmDelete.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mountChakra } from "../lib/testHelpers"; 2 | import { ConfirmDelete } from "./ConfirmDelete"; 3 | 4 | function empty() {} 5 | 6 | beforeEach(() => { 7 | global.isReactLoaded = false; 8 | }); 9 | 10 | const name = "test"; 11 | const buttonText = "button_text"; 12 | const fns = { 13 | confirm: empty, 14 | cancel: empty, 15 | close: empty, 16 | }; 17 | 18 | it("has an input", () => { 19 | mountChakra( 20 | 28 | ); 29 | cy.waitForReact(); 30 | cy.react("ConfirmDelete").get("input").should("exist"); 31 | }); 32 | 33 | it("shows the button text", () => { 34 | mountChakra( 35 | 44 | ); 45 | cy.waitForReact(); 46 | cy.react("ConfirmDelete").get("button").contains(buttonText).should("exist"); 47 | }); 48 | 49 | it("shows the app name", () => { 50 | mountChakra( 51 | 59 | ); 60 | cy.waitForReact(); 61 | cy.react("ConfirmDelete").get("*").contains(name).should("exist"); 62 | }); 63 | 64 | it("disables the button when the input does not contain the app name", () => { 65 | mountChakra( 66 | 75 | ); 76 | cy.waitForReact(); 77 | cy.react("ConfirmDelete") 78 | .get("button") 79 | .contains(buttonText) 80 | .should("be.disabled"); 81 | }); 82 | 83 | it("enables the button when the input contains the app name", () => { 84 | mountChakra( 85 | 94 | ); 95 | cy.waitForReact(); 96 | cy.react("ConfirmDelete").get("input").type(name); 97 | cy.react("ConfirmDelete") 98 | .get("button") 99 | .contains(buttonText) 100 | .should("not.be.disabled"); 101 | }); 102 | 103 | it("calls the confirmation callback", () => { 104 | cy.spy(fns, "confirm"); 105 | 106 | mountChakra( 107 | 116 | ); 117 | cy.waitForReact(); 118 | cy.react("ConfirmDelete").get("input").type(name); 119 | cy.react("ConfirmDelete") 120 | .get("button") 121 | .contains(buttonText) 122 | .click() 123 | .then(() => expect(fns.confirm).to.be.calledOnce); 124 | }); 125 | 126 | it("calls the cancellation callback", () => { 127 | cy.spy(fns, "cancel"); 128 | 129 | mountChakra( 130 | 138 | ); 139 | cy.waitForReact(); 140 | cy.react("ConfirmDelete").get("button").contains("Cancel").click(); 141 | cy.then(() => expect(fns.cancel).to.be.calledOnce); 142 | }); 143 | 144 | it("calls the close callback", () => { 145 | cy.spy(fns, "close"); 146 | 147 | mountChakra( 148 | 157 | ); 158 | cy.waitForReact(); 159 | cy.react("ConfirmDelete").get("input").type(name); 160 | cy.react("ConfirmDelete").get("button").contains(buttonText).click(); 161 | cy.react("ConfirmDelete").get("button").contains("Cancel").click(); 162 | cy.react("ConfirmDelete").get("[aria-label=Close]").click(); 163 | cy.then(() => expect(fns.close).to.be.calledThrice); 164 | }); 165 | -------------------------------------------------------------------------------- /src/components/ConfirmDelete.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Text, 4 | Input, 5 | Modal, 6 | ModalOverlay, 7 | ModalContent, 8 | ModalHeader, 9 | ModalFooter, 10 | ModalBody, 11 | ModalCloseButton, 12 | } from "@chakra-ui/react"; 13 | 14 | import { useState } from "react"; 15 | 16 | export function ConfirmDelete(props: { 17 | name: string; 18 | onConfirmation: () => void; 19 | onCancellation: () => void; 20 | buttonText?: string; 21 | verb?: string; 22 | isOpen: boolean; 23 | onOpen: () => void; 24 | onClose: () => void; 25 | }) { 26 | const { 27 | name, 28 | onConfirmation, 29 | onCancellation, 30 | buttonText, 31 | isOpen, 32 | verb, 33 | onClose, 34 | } = props; 35 | 36 | const [value, setValue] = useState(""); 37 | const [markedForDeletion, setDelete] = useState(false); 38 | 39 | function handleChange(evt) { 40 | const v: string = evt.target.value; 41 | setValue(v); 42 | v.trim().includes(name.trim()) ? setDelete(true) : setDelete(false); 43 | } 44 | 45 | return ( 46 | { 49 | onClose(); 50 | onCancellation(); 51 | }} 52 | > 53 | 54 | 55 | 56 | Are you sure you want to {verb ?? "delete"} {name}? 57 | 58 | 59 | 60 | 61 | 62 | Type {name} into the box below to confirm this 63 | action. 64 | 65 | 72 | 73 | 74 | 83 | 96 | 97 | 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Domain.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Flex, 3 | Heading, 4 | useColorMode, 5 | Box, 6 | Tooltip, 7 | Spinner, 8 | Badge, 9 | Button, 10 | ButtonGroup, 11 | Text, 12 | Table, 13 | Tr, 14 | Td, 15 | Th, 16 | useClipboard, 17 | Thead, 18 | Tbody, 19 | } from "@chakra-ui/react"; 20 | import Icon from "@hackclub/icons"; 21 | import React from "react"; 22 | 23 | function DNSRecordTable({ domain }: { domain: string }) { 24 | const { colorMode } = useColorMode(); 25 | 26 | const split = domain.split("."); 27 | 28 | const { hasCopied, onCopy } = useClipboard( 29 | split.length == 2 ? "167.99.113.134" : "hackclub.app." 30 | ); 31 | 32 | return ( 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 58 | 59 | 60 |
TypeNameValue
{split.length == 2 ? "A" : "CNAME"} 50 | {split.length == 2 ? "@" : split.slice(0, -2).join(".")} 51 | 53 | {split.length == 2 ? "167.99.113.134" : "hackclub.app."}{" "} 54 | 57 |
61 | ); 62 | } 63 | 64 | export default function Domain({ 65 | domain, 66 | verified, 67 | loading = false, 68 | defaultDomain = false, 69 | }: { 70 | domain: string; 71 | verified: boolean; 72 | loading?: boolean; 73 | defaultDomain?: boolean; 74 | }) { 75 | const { colorMode } = useColorMode(); 76 | 77 | return ( 78 | 86 | 87 | {loading ? ( 88 | 89 | 90 | 91 | ) : verified ? ( 92 | 93 | 94 | 95 | 96 | 97 | ) : ( 98 | 99 | 100 | 101 | )} 102 | 103 | {verified ? ( 104 | 110 | 116 | {domain} 117 | 118 | {defaultDomain && ( 119 | 120 | Default 121 | 122 | )} 123 | 124 | 125 | 126 | 127 | ) : ( 128 | 129 | {domain} 130 | 131 | )} 132 | 133 | {loading && ( 134 | 135 | Verifying configuration... 136 | 137 | )} 138 | 139 | 140 | {!verified && ( 141 | 144 | )} 145 | 146 | 147 | 148 | 149 | {!loading && !verified && ( 150 | <> 151 | 152 | Error activating domain. 153 | 154 | 155 | 156 | Please add the following record to your domain's DNS 157 | configuration. 158 | 159 | 160 | 161 | 162 | )} 163 | 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /src/components/KVEntry.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Input, 4 | InputGroup, 5 | InputRightElement, 6 | InputLeftAddon, 7 | Text, 8 | } from "@chakra-ui/react"; 9 | 10 | import { useState, useEffect } from "react"; 11 | import { KVConfig } from "../types/haas"; 12 | 13 | export function KVEntry(props: { 14 | entry: KVConfig; 15 | id: string; 16 | onDataChange: (obj: unknown) => void; 17 | }) { 18 | const { entry, id, onDataChange } = props; 19 | const { obscureValue, keyEditable, valueEditable, key, value } = entry[id]; 20 | const [hide, toggleVal] = useState(obscureValue); 21 | const [val, updateVal] = useState(value); 22 | const [newKey, updateKey] = useState(key); 23 | useEffect(() => { 24 | runCallback(); 25 | }, [val]); 26 | 27 | useEffect(() => { 28 | runCallback(); 29 | }, [newKey]); 30 | 31 | const handleChange = (evt) => { 32 | updateVal(evt.target.value); 33 | }; 34 | const handleKeyChange = (event) => { 35 | updateKey(event.target.value); 36 | }; 37 | function runCallback() { 38 | const obj = {}; 39 | obj[id] = { 40 | obscureValue, 41 | keyEditable, 42 | valueEditable, 43 | key: newKey, 44 | value: val, 45 | }; 46 | onDataChange(obj); 47 | } 48 | return ( 49 | <> 50 | 51 | 52 | {keyEditable ? ( 53 | 59 | ) : ( 60 | {newKey} 61 | )} 62 | 63 | {/* this is here in an attempt to stop browsers from offering to save passwords */} 64 | {hide && } 65 | 75 | 76 | {obscureValue && ( 77 | 87 | )} 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Logs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, CSSProperties } from "react"; 2 | import { 3 | Box, 4 | useColorMode, 5 | FormControl, 6 | FormLabel, 7 | Switch, 8 | } from "@chakra-ui/react"; 9 | 10 | export type LogsProps = { 11 | render: (item: T) => React.ReactElement; 12 | keyer: (item: T) => React.Key; 13 | logs: T[]; 14 | }; 15 | 16 | export default function Logs({ logs, render, keyer }: LogsProps) { 17 | const { colorMode } = useColorMode(); 18 | const [autoScroll, setAutoScroll] = useState(true); 19 | const [wordWrap, setWordWrap] = useState(false); 20 | 21 | const logsElement = useRef(null); 22 | 23 | const wrapStyle: CSSProperties = wordWrap 24 | ? { 25 | whiteSpace: "pre-wrap", 26 | wordWrap: "break-word", 27 | } 28 | : {}; 29 | 30 | useEffect(() => { 31 | if (autoScroll && logsElement.current) { 32 | logsElement.current.scroll({ 33 | top: logsElement.current.scrollHeight, 34 | behavior: "smooth", 35 | }); 36 | } 37 | }, [logs]); 38 | 39 | return ( 40 | <> 41 | 42 | 43 | Auto Scroll 44 | setAutoScroll(!autoScroll)} 48 | /> 49 | 50 | 51 | 52 | Word Wrap 53 | setWordWrap(!wordWrap)} 57 | /> 58 | 59 | 60 | 70 | {logs.map((log) => ( 71 |
81 | 						{render(log)}
82 | 					
83 | ))} 84 |
85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/Stat.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text, Heading } from "@chakra-ui/react"; 2 | 3 | export function Stat({ 4 | label, 5 | description, 6 | style, 7 | }: { 8 | style?: React.CSSProperties; 9 | label: string; 10 | description: string; 11 | }) { 12 | return ( 13 | 14 | 15 | {label} 16 | 17 | {description} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/TeamCreateModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalBody, 6 | Tabs, 7 | Tab, 8 | TabList, 9 | TabPanels, 10 | TabPanel, 11 | } from "@chakra-ui/react"; 12 | 13 | import { FormikHelpers } from "formik"; 14 | import React, { useRef } from "react"; 15 | import TeamCreateForm from "./forms/team-create"; 16 | import TeamJoinForm from "./forms/team-join"; 17 | 18 | type CreateValues = { slug: string; name?: string }; 19 | type JoinValues = { invite: string }; 20 | 21 | export default function TeamCreateModal({ 22 | isOpen, 23 | onClose, 24 | onCreate, 25 | onJoin, 26 | }: { 27 | isOpen: boolean; 28 | onClose: () => void; 29 | onCreate: ( 30 | values: CreateValues, 31 | formikHelpers: FormikHelpers 32 | ) => void | Promise; 33 | onJoin: ( 34 | values: JoinValues, 35 | formikHelpers: FormikHelpers 36 | ) => void | Promise; 37 | }) { 38 | const initialRef = useRef(); 39 | 40 | return ( 41 | 48 | 49 | 50 | 51 | 52 | Create 53 | Join 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from "@chakra-ui/react"; 2 | import Icon from "@hackclub/icons"; 3 | 4 | export function ErrorToast({ text }: { text: string }) { 5 | return ( 6 | 14 | 15 | 16 | {text} 17 | 18 | 19 | ); 20 | } 21 | 22 | export function SuccessToast({ text }: { text: string }) { 23 | return ( 24 | 32 | 33 | 34 | {text} 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/forms/team-create.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormLabel, 4 | InputGroup, 5 | InputLeftAddon, 6 | Input, 7 | FormErrorMessage, 8 | FormHelperText, 9 | Button, 10 | Flex, 11 | Heading, 12 | } from "@chakra-ui/react"; 13 | import { Formik, FormikHelpers } from "formik"; 14 | 15 | type Values = { slug: string; name?: string }; 16 | 17 | export default function TeamCreateForm({ 18 | onClose, 19 | onSubmit, 20 | }: { 21 | onClose: () => void; 22 | onSubmit: ( 23 | values: Values, 24 | formikHelpers: FormikHelpers 25 | ) => void | Promise; 26 | }) { 27 | return ( 28 | { 32 | const errors: Partial = {}; 33 | if (!values.slug) { 34 | errors.slug = "This field is required."; 35 | } else if (!/^[a-z0-9\-]*$/.test(values.slug)) { 36 | errors.slug = 37 | "Your team URL may only contain lowercase letters, numbers, and dashes."; 38 | } 39 | 40 | return errors; 41 | }} 42 | > 43 | {({ 44 | handleChange, 45 | handleBlur, 46 | values, 47 | handleSubmit, 48 | errors, 49 | isSubmitting, 50 | }) => ( 51 |
52 | 53 | 54 | Create A Team 55 | 56 | URL 57 | 58 | https://haas.hackclub.com/teams/ 59 | 69 | 70 | {errors.slug} 71 | 72 | 73 | 74 | Display Name 75 | 85 | {errors.name} 86 | 87 | An optional display name for your team. 88 | 89 | 90 | 93 | 102 | 103 | 104 |
105 | )} 106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/forms/team-join.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | FormLabel, 4 | InputGroup, 5 | InputLeftAddon, 6 | Input, 7 | FormErrorMessage, 8 | FormHelperText, 9 | Button, 10 | Flex, 11 | Heading, 12 | } from "@chakra-ui/react"; 13 | import { Formik, FormikHelpers } from "formik"; 14 | 15 | type Values = { invite: string }; 16 | 17 | export default function TeamJoinForm({ 18 | onClose, 19 | onSubmit, 20 | }: { 21 | onClose: () => void; 22 | onSubmit: ( 23 | values: Values, 24 | formikHelpers: FormikHelpers 25 | ) => void | Promise; 26 | }) { 27 | return ( 28 | { 32 | const errors: Partial = {}; 33 | if (!values.invite) { 34 | errors.invite = "This field is required."; 35 | } 36 | return errors; 37 | }} 38 | > 39 | {({ 40 | handleChange, 41 | handleBlur, 42 | values, 43 | handleSubmit, 44 | errors, 45 | isSubmitting, 46 | }) => ( 47 |
48 | 49 | 50 | Join A Team 51 | 52 | Invite ID 53 | 54 | Invite ID 55 | 65 | 66 | {errors.invite} 67 | 68 | Ask a team member to send you their team's unique invite 69 | code! 70 | 71 | 72 | 73 | 74 | 77 | 86 | 87 |
88 | )} 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/layouts/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | import HaasLayout, { 3 | SidebarBackButton, 4 | SidebarItem, 5 | SidebarItemIcon, 6 | } from "./HaasLayout"; 7 | 8 | import { IApp, ITeam, IUser } from "../types/haas"; 9 | import { Flex, Heading, Tooltip } from "@chakra-ui/react"; 10 | 11 | export default function AppLayout({ 12 | children, 13 | selected, 14 | app, 15 | user, 16 | team, 17 | }: PropsWithChildren<{ 18 | selected: string; 19 | app: IApp; 20 | user: IUser; 21 | team: ITeam; 22 | }>) { 23 | return ( 24 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | {app.slug} 39 | 40 | 41 | 46 | Logs 47 | 48 | 53 | Deploy 54 | 55 | 60 | Domains 61 | 62 | 67 | Addons 68 | 69 | 74 | Environment 75 | 76 | 77 | } 78 | > 79 | {children} 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, IconButton, useDisclosure } from "@chakra-ui/react"; 2 | import HaasLayout, { 3 | SidebarItem, 4 | SidebarSectionHeader, 5 | } from "../layouts/HaasLayout"; 6 | import { IApp, ITeam, IUser } from "../types/haas"; 7 | import Icon from "@hackclub/icons"; 8 | import { useRouter } from "next/router"; 9 | import React, { PropsWithChildren, ReactElement } from "react"; 10 | import fetchApi from "../lib/fetch"; 11 | import TeamCreateModal from "../components/TeamCreateModal"; 12 | import { nanoid } from "nanoid"; 13 | 14 | export default function DashboardLayout({ 15 | user, 16 | teams, 17 | personalApps, 18 | selected, 19 | children, 20 | actionButton, 21 | }: PropsWithChildren<{ 22 | user: IUser; 23 | teams: ITeam[]; 24 | personalApps: IApp[]; 25 | selected: string; 26 | actionButton?: ReactElement; 27 | }>) { 28 | const teamModal = useDisclosure(); 29 | const router = useRouter(); 30 | 31 | return ( 32 | 36 | Personal 37 | 42 | Apps 43 | {!!personalApps.length && ( 44 | {personalApps.length} 45 | )} 46 | 47 | 52 | Settings 53 | 54 | 55 | } 60 | onClick={teamModal.onOpen} 61 | data-cy="create-team" 62 | /> 63 | } 64 | > 65 | Teams 66 | 67 | {teams 68 | .filter((t) => !t.personal) 69 | .map((team) => ( 70 | 76 | {team.name || team.slug} 77 | 78 | ))} 79 | 80 | } 81 | user={user} 82 | actionButton={actionButton} 83 | > 84 | { 88 | try { 89 | // TODO: mutate SWR state after creating team 90 | await fetchApi("/teams", { 91 | headers: { 92 | "Content-Type": "application/json", 93 | }, 94 | method: "POST", 95 | body: JSON.stringify({ 96 | slug: v.slug, 97 | name: v.name, 98 | invite: nanoid(7), 99 | }), 100 | }); 101 | 102 | teamModal.onClose(); 103 | 104 | router.push(`/teams/${v.slug}`); 105 | } catch (e) { 106 | if (e.resp?.status === 409) { 107 | setErrors({ 108 | slug: "This URL is already taken by another team.", 109 | }); 110 | } 111 | } 112 | 113 | setSubmitting(false); 114 | }} 115 | onJoin={async (v, { setErrors, setSubmitting }) => { 116 | try { 117 | await fetchApi(`/teams/${v.invite}/invite`, { 118 | headers: { 119 | "Content-Type": "application/json", 120 | }, 121 | method: "POST", 122 | }); 123 | 124 | teamModal.onClose(); 125 | } catch (e) { 126 | setErrors({ 127 | invite: "Invalid invite code!", 128 | }); 129 | } 130 | 131 | setSubmitting(false); 132 | }} 133 | /> 134 | 135 | {children} 136 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/layouts/HaasLayout.tsx: -------------------------------------------------------------------------------- 1 | import Icon from "@hackclub/icons"; 2 | import NextLink from "next/link"; 3 | import React, { PropsWithChildren, ReactElement } from "react"; 4 | import { 5 | Avatar, 6 | Box, 7 | useBreakpointValue, 8 | Flex, 9 | useDisclosure, 10 | Heading, 11 | IconButton, 12 | SystemStyleObject, 13 | useColorMode, 14 | Tooltip, 15 | Link, 16 | ChakraProps, 17 | forwardRef, 18 | } from "@chakra-ui/react"; 19 | import { Glyph } from "../types/glyph"; 20 | import ColorSwitcher from "../components/ColorButton"; 21 | import { IUser } from "../types/haas"; 22 | 23 | export function SidebarBackButton({ 24 | href, 25 | ...props 26 | }: { href: string } & ChakraProps): ReactElement { 27 | return ( 28 | 29 | 30 | 31 | Back 32 | 33 | 34 | ); 35 | } 36 | 37 | export const SidebarItemIcon = forwardRef( 38 | ( 39 | { 40 | selected = false, 41 | image, 42 | icon, 43 | ...props 44 | }: { selected?: boolean; image?: string; icon?: Glyph }, 45 | ref 46 | ) => { 47 | const { colorMode } = useColorMode(); 48 | 49 | if (image) { 50 | return ( 51 | 61 | ); 62 | } else if (icon) { 63 | return ( 64 | 83 | 84 | 85 | ); 86 | } 87 | } 88 | ); 89 | 90 | export interface SidebarItemProps { 91 | image?: string; 92 | icon?: Glyph; 93 | badge?: string; 94 | href: string; 95 | selected?: boolean; 96 | } 97 | 98 | export function SidebarItem({ 99 | image, 100 | icon, 101 | children, 102 | href, 103 | sx, 104 | selected, 105 | }: PropsWithChildren & { sx?: SystemStyleObject }) { 106 | return ( 107 | 108 | 109 | 117 | {(image || icon) && ( 118 | 119 | )} 120 | 131 | {children} 132 | 133 | 134 | 135 | 136 | ); 137 | } 138 | 139 | export function SidebarSectionHeader({ 140 | actionButton, 141 | children, 142 | }: PropsWithChildren<{ 143 | actionButton?: ReactElement; 144 | }>) { 145 | return ( 146 | 151 | {children} 152 | {actionButton} 153 | 154 | ); 155 | } 156 | 157 | export function SidebarHeader({ 158 | image, 159 | icon, 160 | children, 161 | ...props 162 | }: PropsWithChildren<{ image?: string; icon?: Glyph } & ChakraProps>) { 163 | return ( 164 | 165 | {" "} 166 | {children} 167 | 168 | ); 169 | } 170 | 171 | function AppHeader({ avatar, name }: { avatar?: string; name: string }) { 172 | return ( 173 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | ); 202 | } 203 | 204 | export interface ISidebarSection { 205 | title?: string; 206 | actionButton?: ReactElement; 207 | items: SidebarItemProps[]; 208 | } 209 | 210 | export default function HaasLayout({ 211 | title, 212 | subtitle, 213 | image, 214 | icon, 215 | children, 216 | user, 217 | actionButton, 218 | sidebar, 219 | }: PropsWithChildren<{ 220 | title: string | ReactElement; 221 | image?: string; 222 | icon?: Glyph; 223 | subtitle?: string | ReactElement; 224 | user?: IUser; 225 | actionButton?: ReactElement; 226 | sidebar?: ReactElement; 227 | }>) { 228 | const { colorMode } = useColorMode(); 229 | 230 | let avatar: ReactElement; 231 | const variant = useBreakpointValue({ base: "hide", lg: "show" }, "show"); 232 | 233 | if (image && icon) { 234 | avatar = ( 235 | } 239 | borderRadius={8} 240 | bg={colorMode == "dark" ? "gray.700" : "gray.50"} 241 | color={colorMode == "dark" ? "gray.100" : "black"} 242 | mr={8} 243 | ignoreFallback 244 | /> 245 | ); 246 | } else if (image) { 247 | avatar = ( 248 | 256 | ); 257 | } else if (icon) { 258 | avatar = ( 259 | } 262 | borderRadius={8} 263 | bg={colorMode == "dark" ? "gray.700" : "gray.50"} 264 | color={colorMode == "dark" ? "gray.100" : "black"} 265 | mr={8} 266 | /> 267 | ); 268 | } 269 | 270 | const { isOpen, onOpen, onClose } = useDisclosure(); 271 | 272 | return ( 273 | 274 | {/* TO BE UNCOMMENTED SOON */} 275 | 276 | {/* {variant === "show" ? ( */} 277 | {sidebar} 278 | {/* ) : ( 279 | 280 | 281 | 282 | 283 | 288 | 289 | 290 | 291 | )} */} 292 | 293 | 294 | {variant === "hide" && ( 295 | } 298 | /> 299 | )} 300 | 301 | {avatar} 302 | 303 | 304 | {subtitle && ( 305 | 312 | {subtitle} 313 | 314 | )} 315 | 316 | {title} 317 | 318 | 319 | {actionButton && {actionButton}} 320 | 321 | {children} 322 | 323 | 324 | ); 325 | } 326 | 327 | function Sidebar({ 328 | user, 329 | onClose, 330 | children, 331 | }: PropsWithChildren<{ 332 | user: IUser; 333 | onClose?: () => void; 334 | }>) { 335 | const { colorMode } = useColorMode(); 336 | 337 | return ( 338 | 348 | {onClose && ( 349 | } 354 | onClick={onClose} 355 | /> 356 | )} 357 | 358 | 359 | {children} 360 | 361 | 362 | ); 363 | } 364 | -------------------------------------------------------------------------------- /src/layouts/TeamLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@chakra-ui/react"; 2 | import React, { PropsWithChildren, ReactElement } from "react"; 3 | import { IApp, ITeam, IUser } from "../types/haas"; 4 | import HaasLayout, { 5 | SidebarBackButton, 6 | SidebarHeader, 7 | SidebarItem, 8 | } from "./HaasLayout"; 9 | 10 | export default function TeamLayout({ 11 | children, 12 | selected, 13 | team, 14 | user, 15 | apps, 16 | users, 17 | actionButton, 18 | }: PropsWithChildren<{ 19 | team: ITeam; 20 | selected: string; 21 | user: IUser; 22 | apps: IApp[]; 23 | users: IUser[]; 24 | actionButton?: ReactElement; 25 | }>) { 26 | return ( 27 | 35 | 36 | 37 | 38 | {team.name || team.slug} 39 | 40 | 41 | 46 | Apps 47 | {!!apps.length && {apps.length}} 48 | 49 | 50 | {!team.personal && ( 51 | <> 52 | 57 | Users 58 | {!!users.length && {users.length}} 59 | 60 | 65 | Settings 66 | 67 | 68 | )} 69 | 70 | } 71 | > 72 | {children} 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/dummyData.ts: -------------------------------------------------------------------------------- 1 | import { IAddon } from "../types/haas"; 2 | 3 | export const devAddons: IAddon[] = [ 4 | { 5 | price: "3", 6 | id: "0", 7 | name: "PostgreSQL", 8 | activated: false, 9 | img: "https://upload.wikimedia.org/wikipedia/commons/2/29/Postgresql_elephant.svg", 10 | description: 11 | "PostgreSQL is the best database to ever exist. Sometimes it's hard to understand why people use other databases like . Postgres >>>", 12 | storage: "1.2 GB", 13 | config: { 14 | ddfddfy47: { 15 | key: "USER", 16 | value: "ROOT", 17 | valueEditable: true, 18 | keyEditable: true, 19 | obscureValue: false, 20 | }, 21 | djfusdhf8e74: { 22 | key: "PASSWORD", 23 | value: "iL0V3dA1a", 24 | valueEditable: true, 25 | keyEditable: false, 26 | obscureValue: true, 27 | }, 28 | }, 29 | }, 30 | { 31 | price: "3", 32 | id: "1", 33 | name: "MongoDB", 34 | activated: true, 35 | storage: "3.6 GB", 36 | img: "https://media-exp1.licdn.com/dms/image/C560BAQGC029P7UbAMQ/company-logo_200_200/0/1562088387077?e=2159024400&v=beta&t=lEY4Obku1xJ3BB_BpN3Np9ILy8_zaB1_yjsfH9A57qs", 37 | description: 38 | "MongoDB is the best database to ever exist. Sometimes it's hard to understand why people use other databases like . MongoDB >>>", 39 | config: { 40 | u488h: { 41 | key: "ADMIN_USER", 42 | value: "root", 43 | keyEditable: false, 44 | valueEditable: true, 45 | obscureValue: false, 46 | }, 47 | hfofs9: { 48 | key: "PASSWORD", 49 | value: "uwuowo123", 50 | keyEditable: false, 51 | valueEditable: true, 52 | obscureValue: true, 53 | }, 54 | }, 55 | }, 56 | { 57 | price: "3", 58 | id: "2", 59 | name: "Redis", 60 | activated: false, 61 | storage: "1.9 GB", 62 | img: "https://www.nditech.org/sites/default/files/styles/small_photo/public/redis-logo.png?itok=LrULOkWT", 63 | description: 64 | "Redis is the best database to ever exist. Sometimes it's hard to understand why people use other databases like . Redis >>>", 65 | config: { 66 | ddfsfyy5: { 67 | key: "ADMIN_USER", 68 | value: "root", 69 | keyEditable: false, 70 | valueEditable: true, 71 | obscureValue: false, 72 | }, 73 | idsf8: { 74 | key: "PASSWORD", 75 | value: "uwuowo123", 76 | keyEditable: false, 77 | valueEditable: true, 78 | obscureValue: true, 79 | }, 80 | }, 81 | }, 82 | ]; 83 | 84 | export const devAddonsOriginal: IAddon[] = devAddons; 85 | -------------------------------------------------------------------------------- /src/lib/failBuildEventLogs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "stream": "Step 1/27 : FROM python:alpine AS base" }, 3 | { "stream": "\n" }, 4 | { "id": "alpine", "status": "Pulling from library/python" }, 5 | { "id": "9b3977197b4f", "status": "Already exists", "progressDetail": {} }, 6 | { "id": "8d32640a910c", "status": "Pulling fs layer", "progressDetail": {} }, 7 | { "id": "447b5382bfed", "status": "Pulling fs layer", "progressDetail": {} }, 8 | { "id": "ca719a80a694", "status": "Pulling fs layer", "progressDetail": {} }, 9 | { "id": "2483a1013bef", "status": "Pulling fs layer", "progressDetail": {} }, 10 | { "id": "2483a1013bef", "status": "Waiting", "progressDetail": {} }, 11 | { 12 | "id": "ca719a80a694", 13 | "status": "Downloading", 14 | "progress": "[==================================================>] 233B/233B", 15 | "progressDetail": { "current": 233, "total": 233 } 16 | }, 17 | { 18 | "id": "ca719a80a694", 19 | "status": "Verifying Checksum", 20 | "progressDetail": {} 21 | }, 22 | { "id": "ca719a80a694", "status": "Download complete", "progressDetail": {} }, 23 | { 24 | "id": "8d32640a910c", 25 | "status": "Downloading", 26 | "progress": "[> ] 7.501kB/680kB", 27 | "progressDetail": { "current": 7501, "total": 680008 } 28 | }, 29 | { 30 | "id": "447b5382bfed", 31 | "status": "Downloading", 32 | "progress": "[> ] 118.7kB/11.78MB", 33 | "progressDetail": { "current": 118664, "total": 11777540 } 34 | }, 35 | { 36 | "id": "8d32640a910c", 37 | "status": "Downloading", 38 | "progress": "[==================================================>] 680kB/680kB", 39 | "progressDetail": { "current": 680008, "total": 680008 } 40 | }, 41 | { 42 | "id": "8d32640a910c", 43 | "status": "Verifying Checksum", 44 | "progressDetail": {} 45 | }, 46 | { "id": "8d32640a910c", "status": "Download complete", "progressDetail": {} }, 47 | { 48 | "id": "8d32640a910c", 49 | "status": "Extracting", 50 | "progress": "[==> ] 32.77kB/680kB", 51 | "progressDetail": { "current": 32768, "total": 680008 } 52 | }, 53 | { 54 | "id": "447b5382bfed", 55 | "status": "Downloading", 56 | "progress": "[======> ] 1.42MB/11.78MB", 57 | "progressDetail": { "current": 1419644, "total": 11777540 } 58 | }, 59 | { 60 | "id": "8d32640a910c", 61 | "status": "Extracting", 62 | "progress": "[====================================> ] 491.5kB/680kB", 63 | "progressDetail": { "current": 491520, "total": 680008 } 64 | }, 65 | { 66 | "id": "447b5382bfed", 67 | "status": "Downloading", 68 | "progress": "[=================> ] 4.029MB/11.78MB", 69 | "progressDetail": { "current": 4028796, "total": 11777540 } 70 | }, 71 | { 72 | "id": "8d32640a910c", 73 | "status": "Extracting", 74 | "progress": "[==================================================>] 680kB/680kB", 75 | "progressDetail": { "current": 680008, "total": 680008 } 76 | }, 77 | { 78 | "id": "8d32640a910c", 79 | "status": "Extracting", 80 | "progress": "[==================================================>] 680kB/680kB", 81 | "progressDetail": { "current": 680008, "total": 680008 } 82 | }, 83 | { "id": "8d32640a910c", "status": "Pull complete", "progressDetail": {} }, 84 | { 85 | "id": "447b5382bfed", 86 | "status": "Downloading", 87 | "progress": "[=============================> ] 7.027MB/11.78MB", 88 | "progressDetail": { "current": 7027068, "total": 11777540 } 89 | }, 90 | { 91 | "id": "447b5382bfed", 92 | "status": "Downloading", 93 | "progress": "[==========================================> ] 10.12MB/11.78MB", 94 | "progressDetail": { "current": 10119548, "total": 11777540 } 95 | }, 96 | { 97 | "id": "447b5382bfed", 98 | "status": "Verifying Checksum", 99 | "progressDetail": {} 100 | }, 101 | { "id": "447b5382bfed", "status": "Download complete", "progressDetail": {} }, 102 | { 103 | "id": "447b5382bfed", 104 | "status": "Extracting", 105 | "progress": "[> ] 131.1kB/11.78MB", 106 | "progressDetail": { "current": 131072, "total": 11777540 } 107 | }, 108 | { 109 | "id": "2483a1013bef", 110 | "status": "Downloading", 111 | "progress": "[> ] 23.93kB/2.351MB", 112 | "progressDetail": { "current": 23927, "total": 2350550 } 113 | }, 114 | { 115 | "id": "447b5382bfed", 116 | "status": "Extracting", 117 | "progress": "[===============> ] 3.539MB/11.78MB", 118 | "progressDetail": { "current": 3538944, "total": 11777540 } 119 | }, 120 | { 121 | "id": "2483a1013bef", 122 | "status": "Downloading", 123 | "progress": "[====================> ] 952.7kB/2.351MB", 124 | "progressDetail": { "current": 952703, "total": 2350550 } 125 | }, 126 | { 127 | "id": "447b5382bfed", 128 | "status": "Extracting", 129 | "progress": "[===========================> ] 6.554MB/11.78MB", 130 | "progressDetail": { "current": 6553600, "total": 11777540 } 131 | }, 132 | { "id": "2483a1013bef", "status": "Download complete", "progressDetail": {} }, 133 | { 134 | "id": "447b5382bfed", 135 | "status": "Extracting", 136 | "progress": "[=============================================> ] 10.75MB/11.78MB", 137 | "progressDetail": { "current": 10747904, "total": 11777540 } 138 | }, 139 | { 140 | "id": "447b5382bfed", 141 | "status": "Extracting", 142 | "progress": "[==================================================>] 11.78MB/11.78MB", 143 | "progressDetail": { "current": 11777540, "total": 11777540 } 144 | }, 145 | { "id": "447b5382bfed", "status": "Pull complete", "progressDetail": {} }, 146 | { 147 | "id": "ca719a80a694", 148 | "status": "Extracting", 149 | "progress": "[==================================================>] 233B/233B", 150 | "progressDetail": { "current": 233, "total": 233 } 151 | }, 152 | { 153 | "id": "ca719a80a694", 154 | "status": "Extracting", 155 | "progress": "[==================================================>] 233B/233B", 156 | "progressDetail": { "current": 233, "total": 233 } 157 | }, 158 | { "id": "ca719a80a694", "status": "Pull complete", "progressDetail": {} }, 159 | { 160 | "id": "2483a1013bef", 161 | "status": "Extracting", 162 | "progress": "[> ] 32.77kB/2.351MB", 163 | "progressDetail": { "current": 32768, "total": 2350550 } 164 | }, 165 | { 166 | "id": "2483a1013bef", 167 | "status": "Extracting", 168 | "progress": "[==================================> ] 1.638MB/2.351MB", 169 | "progressDetail": { "current": 1638400, "total": 2350550 } 170 | }, 171 | { 172 | "id": "2483a1013bef", 173 | "status": "Extracting", 174 | "progress": "[==================================================>] 2.351MB/2.351MB", 175 | "progressDetail": { "current": 2350550, "total": 2350550 } 176 | }, 177 | { 178 | "id": "2483a1013bef", 179 | "status": "Extracting", 180 | "progress": "[==================================================>] 2.351MB/2.351MB", 181 | "progressDetail": { "current": 2350550, "total": 2350550 } 182 | }, 183 | { "id": "2483a1013bef", "status": "Pull complete", "progressDetail": {} }, 184 | { 185 | "status": "Digest: sha256:dce56d40d885d2c8847aa2a278a29d50450c8e3d10f9d7ffeb2f38dcc1eb0ea4" 186 | }, 187 | { "status": "Status: Downloaded newer image for python:alpine" }, 188 | { "stream": " ---> 5294fccbb995\n" }, 189 | { "stream": "Step 2/27 : WORKDIR /app" }, 190 | { "stream": "\n" }, 191 | { "stream": " ---> Running in e51ee8509008\n" }, 192 | { "stream": "Removing intermediate container e51ee8509008\n" }, 193 | { "stream": " ---> 30b2263bb400\n" }, 194 | { "stream": "Step 3/27 : COPY requirements.txt ." }, 195 | { "stream": "\n" }, 196 | { "stream": " ---> b6c172bded19\n" }, 197 | { "stream": "Step 4/27 : RUN pip install -r requirements.txt" }, 198 | { "stream": "\n" }, 199 | { "stream": " ---> Running in 11af9215a745\n" }, 200 | { "stream": "Collecting mkdocs==1.2.3\n" }, 201 | { "stream": " Downloading mkdocs-1.2.3-py3-none-any.whl (6.4 MB)\n" }, 202 | { "stream": "Collecting mkdocs-material==4.6.3\n" }, 203 | { 204 | "stream": " Downloading mkdocs_material-4.6.3-py2.py3-none-any.whl (723 kB)\n" 205 | }, 206 | { "stream": "Collecting mkdocs-minify-plugin==0.2.3\n" }, 207 | { "stream": " Downloading mkdocs-minify-plugin-0.2.3.tar.gz (3.0 kB)\n" }, 208 | { "stream": "Collecting pygments==2.7.4\n" }, 209 | { "stream": " Downloading Pygments-2.7.4-py3-none-any.whl (950 kB)\n" }, 210 | { "stream": "Collecting pymdown-extensions==7.0\n" }, 211 | { 212 | "stream": " Downloading pymdown_extensions-7.0-py2.py3-none-any.whl (204 kB)\n" 213 | }, 214 | { "stream": "Collecting packaging>=20.5\n" }, 215 | { "stream": " Downloading packaging-21.3-py3-none-any.whl (40 kB)\n" }, 216 | { "stream": "Collecting Markdown>=3.2.1\n" }, 217 | { "stream": " Downloading Markdown-3.3.6-py3-none-any.whl (97 kB)\n" }, 218 | { "stream": "Collecting mergedeep>=1.3.4\n" }, 219 | { "stream": " Downloading mergedeep-1.3.4-py3-none-any.whl (6.4 kB)\n" }, 220 | { "stream": "Collecting importlib-metadata>=3.10\n" }, 221 | { 222 | "stream": " Downloading importlib_metadata-4.10.0-py3-none-any.whl (17 kB)\n" 223 | }, 224 | { "stream": "Collecting Jinja2>=2.10.1\n" }, 225 | { "stream": " Downloading Jinja2-3.0.3-py3-none-any.whl (133 kB)\n" }, 226 | { "stream": "Collecting click>=3.3\n" }, 227 | { "stream": " Downloading click-8.0.3-py3-none-any.whl (97 kB)\n" }, 228 | { "stream": "Collecting watchdog>=2.0\n" }, 229 | { "stream": " Downloading watchdog-2.1.6.tar.gz (107 kB)\n" }, 230 | { "stream": "Collecting ghp-import>=1.0\n" }, 231 | { "stream": " Downloading ghp_import-2.0.2-py3-none-any.whl (11 kB)\n" }, 232 | { "stream": "Collecting pyyaml-env-tag>=0.1\n" }, 233 | { "stream": " Downloading pyyaml_env_tag-0.1-py3-none-any.whl (3.9 kB)\n" }, 234 | { "stream": "Collecting PyYAML>=3.10\n" }, 235 | { "stream": " Downloading PyYAML-6.0.tar.gz (124 kB)\n" }, 236 | { "stream": " Installing build dependencies: started\n" }, 237 | { 238 | "stream": " Installing build dependencies: finished with status 'done'\n" 239 | }, 240 | { "stream": " Getting requirements to build wheel: started\n" }, 241 | { 242 | "stream": " Getting requirements to build wheel: finished with status 'done'\n" 243 | }, 244 | { "stream": " Preparing wheel metadata: started\n" }, 245 | { "stream": " Preparing wheel metadata: finished with status 'done'\n" }, 246 | { "stream": "Collecting htmlmin>=0.1.4\n" }, 247 | { "stream": " Downloading htmlmin-0.1.12.tar.gz (19 kB)\n" }, 248 | { "stream": "Collecting jsmin>=2.2.2\n" }, 249 | { "stream": " Downloading jsmin-3.0.0.tar.gz (11 kB)\n" }, 250 | { "stream": "Collecting python-dateutil>=2.8.1\n" }, 251 | { 252 | "stream": " Downloading python_dateutil-2.8.2-py2.py3-none-any.whl (247 kB)\n" 253 | }, 254 | { "stream": "Collecting zipp>=0.5\n" }, 255 | { "stream": " Downloading zipp-3.6.0-py3-none-any.whl (5.3 kB)\n" }, 256 | { "stream": "Collecting MarkupSafe>=2.0\n" }, 257 | { 258 | "stream": " Downloading MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl (31 kB)\n" 259 | }, 260 | { "stream": "Collecting pyparsing!=3.0.5,>=2.0.2\n" }, 261 | { "stream": " Downloading pyparsing-3.0.6-py3-none-any.whl (97 kB)\n" }, 262 | { "stream": "Collecting six>=1.5\n" }, 263 | { "stream": " Downloading six-1.16.0-py2.py3-none-any.whl (11 kB)\n" }, 264 | { 265 | "stream": "Building wheels for collected packages: mkdocs-minify-plugin, htmlmin, jsmin, PyYAML, watchdog\n" 266 | }, 267 | { 268 | "stream": " Building wheel for mkdocs-minify-plugin (setup.py): started\n" 269 | }, 270 | { 271 | "stream": " Building wheel for mkdocs-minify-plugin (setup.py): finished with status 'done'\n" 272 | }, 273 | { 274 | "stream": " Created wheel for mkdocs-minify-plugin: filename=mkdocs_minify_plugin-0.2.3-py3-none-any.whl size=2997 sha256=f93645fd7c99e35a3610d48e88affc553acf1a9db30d2d350837919404afdd88\n Stored in directory: /root/.cache/pip/wheels/9a/19/e8/18d4d04faba557ff1743cbc79d2b2ba3154090d0baddbb1c15\n" 275 | }, 276 | { "stream": " Building wheel for htmlmin (setup.py): started\n" }, 277 | { 278 | "stream": " Building wheel for htmlmin (setup.py): finished with status 'done'\n" 279 | }, 280 | { 281 | "stream": " Created wheel for htmlmin: filename=htmlmin-0.1.12-py3-none-any.whl size=27098 sha256=1803ec9c9ce696aaf2ace632014c2c326dec8b251624a2d0e94c7c37e3c2dac0\n Stored in directory: /root/.cache/pip/wheels/dd/91/29/a79cecb328d01739e64017b6fb9a1ab9d8cb1853098ec5966d\n" 282 | }, 283 | { "stream": " Building wheel for jsmin (setup.py): started\n" }, 284 | { 285 | "stream": " Building wheel for jsmin (setup.py): finished with status 'done'\n" 286 | }, 287 | { 288 | "stream": " Created wheel for jsmin: filename=jsmin-3.0.0-py3-none-any.whl size=13894 sha256=af7029f38e3ab9a61c00f63a30028b602d05903a3b944b7e52466ee70abcd52c\n" 289 | }, 290 | { 291 | "stream": " Stored in directory: /root/.cache/pip/wheels/8b/88/9e/36445302a06032ebfff429cd671b023adc17477c821488a6f2\n" 292 | }, 293 | { "stream": " Building wheel for PyYAML (PEP 517): started\n" }, 294 | { 295 | "stream": " Building wheel for PyYAML (PEP 517): finished with status 'done'\n" 296 | }, 297 | { 298 | "stream": " Created wheel for PyYAML: filename=PyYAML-6.0-cp310-cp310-linux_aarch64.whl size=45332 sha256=18d70dd41f13e5bcb8bb9dc939f67005cdb2662033556460876d128377a11e5d\n Stored in directory: /root/.cache/pip/wheels/1d/f3/b4/4aea0992adbed14b36ce9c3857d3707c762a4374479230685d\n" 299 | }, 300 | { "stream": " Building wheel for watchdog (setup.py): started\n" }, 301 | { 302 | "stream": " Building wheel for watchdog (setup.py): finished with status 'done'\n" 303 | }, 304 | { 305 | "stream": " Created wheel for watchdog: filename=watchdog-2.1.6-py3-none-any.whl size=76456 sha256=4846fd230af8bdbc877c2fb4776bbd63e758df685ec75712b7d2f0967b210f57\n Stored in directory: /root/.cache/pip/wheels/9a/86/44/ad0012a0b13b6a4bb17d60cab3a2b5cec02547ab17dce9b354\n" 306 | }, 307 | { 308 | "stream": "Successfully built mkdocs-minify-plugin htmlmin jsmin PyYAML watchdog\n" 309 | }, 310 | { 311 | "stream": "Installing collected packages: six, zipp, PyYAML, python-dateutil, pyparsing, MarkupSafe, watchdog, pyyaml-env-tag, packaging, mergedeep, Markdown, Jinja2, importlib-metadata, ghp-import, click, pymdown-extensions, pygments, mkdocs, jsmin, htmlmin, mkdocs-minify-plugin, mkdocs-material\n" 312 | }, 313 | { 314 | "stream": "Successfully installed Jinja2-3.0.3 Markdown-3.3.6 MarkupSafe-2.0.1 PyYAML-6.0 click-8.0.3 ghp-import-2.0.2 htmlmin-0.1.12 importlib-metadata-4.10.0 jsmin-3.0.0 mergedeep-1.3.4 mkdocs-1.2.3 mkdocs-material-4.6.3 mkdocs-minify-plugin-0.2.3 packaging-21.3 pygments-2.7.4 pymdown-extensions-7.0 pyparsing-3.0.6 python-dateutil-2.8.2 pyyaml-env-tag-0.1 six-1.16.0 watchdog-2.1.6 zipp-3.6.0\n" 315 | }, 316 | { 317 | "stream": "\u001b[91mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\n\u001b[0m" 318 | }, 319 | { 320 | "stream": "\u001b[91mWARNING: You are using pip version 21.2.4; however, version 21.3.1 is available.\nYou should consider upgrading via the '/usr/local/bin/python -m pip install --upgrade pip' command.\n\u001b[0m" 321 | }, 322 | { "stream": "Removing intermediate container 11af9215a745\n" }, 323 | { "stream": " ---> 25b5667b8b37\n" }, 324 | { 325 | "aux": { 326 | "ID": "sha256:25b5667b8b374700355664bed25b03992892d209defec0cdf541f5405a1858d1" 327 | } 328 | }, 329 | { "stream": "Step 5/27 : FROM node:12-alpine AS app-base" }, 330 | { "stream": "\n" }, 331 | { "id": "12-alpine", "status": "Pulling from library/node" }, 332 | { "id": "be307f383ecc", "status": "Pulling fs layer", "progressDetail": {} }, 333 | { "id": "5f1a4fb2c7eb", "status": "Pulling fs layer", "progressDetail": {} }, 334 | { "id": "303d48716aa7", "status": "Pulling fs layer", "progressDetail": {} }, 335 | { "id": "f5a9282b20ab", "status": "Pulling fs layer", "progressDetail": {} }, 336 | { "id": "f5a9282b20ab", "status": "Waiting", "progressDetail": {} }, 337 | { 338 | "id": "be307f383ecc", 339 | "status": "Downloading", 340 | "progress": "[> ] 28.04kB/2.718MB", 341 | "progressDetail": { "current": 28035, "total": 2717700 } 342 | }, 343 | { 344 | "id": "5f1a4fb2c7eb", 345 | "status": "Downloading", 346 | "progress": "[> ] 257.3kB/24.81MB", 347 | "progressDetail": { "current": 257274, "total": 24809889 } 348 | }, 349 | { 350 | "id": "303d48716aa7", 351 | "status": "Downloading", 352 | "progress": "[> ] 25.3kB/2.426MB", 353 | "progressDetail": { "current": 25297, "total": 2426161 } 354 | }, 355 | { 356 | "id": "be307f383ecc", 357 | "status": "Downloading", 358 | "progress": "[=====================> ] 1.191MB/2.718MB", 359 | "progressDetail": { "current": 1191163, "total": 2717700 } 360 | }, 361 | { 362 | "id": "5f1a4fb2c7eb", 363 | "status": "Downloading", 364 | "progress": "[===> ] 1.568MB/24.81MB", 365 | "progressDetail": { "current": 1567994, "total": 24809889 } 366 | }, 367 | { 368 | "id": "303d48716aa7", 369 | "status": "Downloading", 370 | "progress": "[===========> ] 556.3kB/2.426MB", 371 | "progressDetail": { "current": 556273, "total": 2426161 } 372 | }, 373 | { 374 | "id": "be307f383ecc", 375 | "status": "Downloading", 376 | "progress": "[==============================================> ] 2.505MB/2.718MB", 377 | "progressDetail": { "current": 2505091, "total": 2717700 } 378 | }, 379 | { 380 | "id": "be307f383ecc", 381 | "status": "Verifying Checksum", 382 | "progressDetail": {} 383 | }, 384 | { "id": "be307f383ecc", "status": "Download complete", "progressDetail": {} }, 385 | { 386 | "id": "be307f383ecc", 387 | "status": "Extracting", 388 | "progress": "[> ] 32.77kB/2.718MB", 389 | "progressDetail": { "current": 32768, "total": 2717700 } 390 | }, 391 | { 392 | "id": "303d48716aa7", 393 | "status": "Downloading", 394 | "progress": "[===========================> ] 1.342MB/2.426MB", 395 | "progressDetail": { "current": 1341827, "total": 2426161 } 396 | }, 397 | { 398 | "id": "5f1a4fb2c7eb", 399 | "status": "Downloading", 400 | "progress": "[======> ] 3.136MB/24.81MB", 401 | "progressDetail": { "current": 3135858, "total": 24809889 } 402 | }, 403 | { 404 | "id": "be307f383ecc", 405 | "status": "Extracting", 406 | "progress": "[================================> ] 1.769MB/2.718MB", 407 | "progressDetail": { "current": 1769472, "total": 2717700 } 408 | }, 409 | { 410 | "id": "303d48716aa7", 411 | "status": "Downloading", 412 | "progress": "[===============================================> ] 2.325MB/2.426MB", 413 | "progressDetail": { "current": 2324867, "total": 2426161 } 414 | }, 415 | { 416 | "id": "303d48716aa7", 417 | "status": "Verifying Checksum", 418 | "progressDetail": {} 419 | }, 420 | { "id": "303d48716aa7", "status": "Download complete", "progressDetail": {} }, 421 | { 422 | "id": "be307f383ecc", 423 | "status": "Extracting", 424 | "progress": "[==================================================>] 2.718MB/2.718MB", 425 | "progressDetail": { "current": 2717700, "total": 2717700 } 426 | }, 427 | { 428 | "id": "be307f383ecc", 429 | "status": "Extracting", 430 | "progress": "[==================================================>] 2.718MB/2.718MB", 431 | "progressDetail": { "current": 2717700, "total": 2717700 } 432 | }, 433 | { 434 | "id": "5f1a4fb2c7eb", 435 | "status": "Downloading", 436 | "progress": "[===========> ] 5.458MB/24.81MB", 437 | "progressDetail": { "current": 5458290, "total": 24809889 } 438 | }, 439 | { "id": "be307f383ecc", "status": "Pull complete", "progressDetail": {} }, 440 | { 441 | "id": "5f1a4fb2c7eb", 442 | "status": "Downloading", 443 | "progress": "[=================> ] 8.571MB/24.81MB", 444 | "progressDetail": { "current": 8571250, "total": 24809889 } 445 | }, 446 | { 447 | "id": "5f1a4fb2c7eb", 448 | "status": "Downloading", 449 | "progress": "[=======================> ] 11.7MB/24.81MB", 450 | "progressDetail": { "current": 11696498, "total": 24809889 } 451 | }, 452 | { 453 | "id": "5f1a4fb2c7eb", 454 | "status": "Downloading", 455 | "progress": "[=============================> ] 14.81MB/24.81MB", 456 | "progressDetail": { "current": 14805362, "total": 24809889 } 457 | }, 458 | { 459 | "id": "5f1a4fb2c7eb", 460 | "status": "Downloading", 461 | "progress": "[====================================> ] 17.93MB/24.81MB", 462 | "progressDetail": { "current": 17930610, "total": 24809889 } 463 | }, 464 | { 465 | "id": "f5a9282b20ab", 466 | "status": "Downloading", 467 | "progress": "[==================================================>] 449B/449B", 468 | "progressDetail": { "current": 449, "total": 449 } 469 | }, 470 | { 471 | "id": "f5a9282b20ab", 472 | "status": "Verifying Checksum", 473 | "progressDetail": {} 474 | }, 475 | { "id": "f5a9282b20ab", "status": "Download complete", "progressDetail": {} }, 476 | { 477 | "id": "5f1a4fb2c7eb", 478 | "status": "Downloading", 479 | "progress": "[==========================================> ] 21.06MB/24.81MB", 480 | "progressDetail": { "current": 21064050, "total": 24809889 } 481 | }, 482 | { 483 | "id": "5f1a4fb2c7eb", 484 | "status": "Downloading", 485 | "progress": "[================================================> ] 24.19MB/24.81MB", 486 | "progressDetail": { "current": 24185202, "total": 24809889 } 487 | }, 488 | { 489 | "id": "5f1a4fb2c7eb", 490 | "status": "Verifying Checksum", 491 | "progressDetail": {} 492 | }, 493 | { "id": "5f1a4fb2c7eb", "status": "Download complete", "progressDetail": {} }, 494 | { 495 | "id": "5f1a4fb2c7eb", 496 | "status": "Extracting", 497 | "progress": "[> ] 262.1kB/24.81MB", 498 | "progressDetail": { "current": 262144, "total": 24809889 } 499 | }, 500 | { 501 | "id": "5f1a4fb2c7eb", 502 | "status": "Extracting", 503 | "progress": "[==========> ] 4.981MB/24.81MB", 504 | "progressDetail": { "current": 4980736, "total": 24809889 } 505 | }, 506 | { 507 | "id": "5f1a4fb2c7eb", 508 | "status": "Extracting", 509 | "progress": "[====================> ] 9.961MB/24.81MB", 510 | "progressDetail": { "current": 9961472, "total": 24809889 } 511 | }, 512 | { 513 | "id": "5f1a4fb2c7eb", 514 | "status": "Extracting", 515 | "progress": "[=============================> ] 14.68MB/24.81MB", 516 | "progressDetail": { "current": 14680064, "total": 24809889 } 517 | }, 518 | { 519 | "id": "5f1a4fb2c7eb", 520 | "status": "Extracting", 521 | "progress": "[======================================> ] 18.87MB/24.81MB", 522 | "progressDetail": { "current": 18874368, "total": 24809889 } 523 | }, 524 | { 525 | "id": "5f1a4fb2c7eb", 526 | "status": "Extracting", 527 | "progress": "[==========================================> ] 21.23MB/24.81MB", 528 | "progressDetail": { "current": 21233664, "total": 24809889 } 529 | }, 530 | { 531 | "id": "5f1a4fb2c7eb", 532 | "status": "Extracting", 533 | "progress": "[=============================================> ] 22.54MB/24.81MB", 534 | "progressDetail": { "current": 22544384, "total": 24809889 } 535 | }, 536 | { 537 | "id": "5f1a4fb2c7eb", 538 | "status": "Extracting", 539 | "progress": "[=================================================> ] 24.38MB/24.81MB", 540 | "progressDetail": { "current": 24379392, "total": 24809889 } 541 | }, 542 | { 543 | "id": "5f1a4fb2c7eb", 544 | "status": "Extracting", 545 | "progress": "[==================================================>] 24.81MB/24.81MB", 546 | "progressDetail": { "current": 24809889, "total": 24809889 } 547 | }, 548 | { "id": "5f1a4fb2c7eb", "status": "Pull complete", "progressDetail": {} }, 549 | { 550 | "id": "303d48716aa7", 551 | "status": "Extracting", 552 | "progress": "[> ] 32.77kB/2.426MB", 553 | "progressDetail": { "current": 32768, "total": 2426161 } 554 | }, 555 | { 556 | "id": "303d48716aa7", 557 | "status": "Extracting", 558 | "progress": "[==================================================>] 2.426MB/2.426MB", 559 | "progressDetail": { "current": 2426161, "total": 2426161 } 560 | }, 561 | { "id": "303d48716aa7", "status": "Pull complete", "progressDetail": {} }, 562 | { 563 | "id": "f5a9282b20ab", 564 | "status": "Extracting", 565 | "progress": "[==================================================>] 449B/449B", 566 | "progressDetail": { "current": 449, "total": 449 } 567 | }, 568 | { 569 | "id": "f5a9282b20ab", 570 | "status": "Extracting", 571 | "progress": "[==================================================>] 449B/449B", 572 | "progressDetail": { "current": 449, "total": 449 } 573 | }, 574 | { "id": "f5a9282b20ab", "status": "Pull complete", "progressDetail": {} }, 575 | { 576 | "status": "Digest: sha256:8fad09b7620b2bc715cbba92e3313c64a797e453f560118576f1740a44584d5d" 577 | }, 578 | { "status": "Status: Downloaded newer image for node:12-alpine" }, 579 | { "stream": " ---> ce1b097ce235\n" }, 580 | { "stream": "Step 6/27 : WORKDIR /app" }, 581 | { "stream": "\n" }, 582 | { "stream": " ---> Running in 9e445d2dbf28\n" }, 583 | { "stream": "Removing intermediate container 9e445d2dbf28\n" }, 584 | { "stream": " ---> 6b2dc63449bc\n" }, 585 | { "stream": "Step 7/27 : COPY app/package.json app/yarn.lock ./" }, 586 | { "stream": "\n" }, 587 | { "stream": " ---> 83b1ce0f11e6\n" }, 588 | { "stream": "Step 8/27 : COPY app/spec ./spec" }, 589 | { "stream": "\n" }, 590 | { "stream": " ---> 58ee302b6f78\n" }, 591 | { "stream": "Step 9/27 : COPY app/src ./src" }, 592 | { "stream": "\n" }, 593 | { "stream": " ---> 2b0385f8cf95\n" }, 594 | { 595 | "aux": { 596 | "ID": "sha256:2b0385f8cf95f48bf6bbdd8e92db5de818043351a9a7ef6e00a063eb16c393f2" 597 | } 598 | }, 599 | { "stream": "Step 10/27 : FROM app-base AS test" }, 600 | { "stream": "\n" }, 601 | { "stream": " ---> 2b0385f8cf95\n" }, 602 | { "stream": "Step 11/27 : RUN apk add --no-cache python3 g++ make" }, 603 | { "stream": "\n" }, 604 | { "stream": " ---> Running in 4cedae576611\n" }, 605 | { 606 | "stream": "fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/aarch64/APKINDEX.tar.gz\n" 607 | }, 608 | { 609 | "stream": "fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/aarch64/APKINDEX.tar.gz\n" 610 | }, 611 | { "stream": "(1/24) Installing binutils (2.35.2-r2)\n" }, 612 | { "stream": "(2/24) Installing libgomp (10.3.1_git20210424-r2)\n" }, 613 | { "stream": "(3/24) Installing libatomic (10.3.1_git20210424-r2)\n" }, 614 | { "stream": "(4/24) Installing libgphobos (10.3.1_git20210424-r2)\n" }, 615 | { "stream": "(5/24) Installing gmp (6.2.1-r0)\n" }, 616 | { "stream": "(6/24) Installing isl22 (0.22-r0)\n" }, 617 | { "stream": "(7/24) Installing mpfr4 (4.1.0-r0)\n" }, 618 | { "stream": "(8/24) Installing mpc1 (1.2.1-r0)\n" }, 619 | { "stream": "(9/24) Installing gcc (10.3.1_git20210424-r2)\n" }, 620 | { "stream": "(10/24) Installing musl-dev (1.2.2-r3)\n" }, 621 | { "stream": "(11/24) Installing libc-dev (0.7.2-r3)\n" }, 622 | { "stream": "(12/24) Installing g++ (10.3.1_git20210424-r2)\n" }, 623 | { "stream": "(13/24) Installing make (4.3-r0)\n" }, 624 | { "stream": "(14/24) Installing libbz2 (1.0.8-r1)\n" }, 625 | { "stream": "(15/24) Installing expat (2.4.1-r0)\n" }, 626 | { "stream": "(16/24) Installing libffi (3.3-r2)\n" }, 627 | { "stream": "(17/24) Installing gdbm (1.19-r0)\n" }, 628 | { "stream": "(18/24) Installing xz-libs (5.2.5-r0)\n" }, 629 | { "stream": "(19/24) Installing mpdecimal (2.5.1-r1)\n" }, 630 | { "stream": "(20/24) Installing ncurses-terminfo-base (6.2_p20210612-r0)\n" }, 631 | { "stream": "(21/24) Installing ncurses-libs (6.2_p20210612-r0)\n" }, 632 | { "stream": "(22/24) Installing readline (8.1.0-r0)\n" }, 633 | { "stream": "(23/24) Installing sqlite-libs (3.35.5-r0)\n" }, 634 | { "stream": "(24/24) Installing python3 (3.9.5-r2)\n" }, 635 | { "stream": "Executing busybox-1.33.1-r6.trigger\n" }, 636 | { "stream": "OK: 236 MiB in 40 packages\n" }, 637 | { "stream": "Removing intermediate container 4cedae576611\n" }, 638 | { "stream": " ---> cf5dc62c5569\n" }, 639 | { "stream": "Step 12/27 : RUN yarn install" }, 640 | { "stream": "\n" }, 641 | { "stream": " ---> Running in 447914b7c511\n" }, 642 | { "stream": "yarn install v1.22.17\n" }, 643 | { "stream": "[1/4] Resolving packages...\n" }, 644 | { 645 | "stream": "\u001b[91mwarning Resolution field \"ansi-regex@5.0.1\" is incompatible with requested version \"ansi-regex@^2.0.0\"\n\u001b[0m" 646 | }, 647 | { 648 | "stream": "\u001b[91mwarning Resolution field \"ansi-regex@5.0.1\" is incompatible with requested version \"ansi-regex@^3.0.0\"\n\u001b[0m" 649 | }, 650 | { 651 | "stream": "\u001b[91mwarning sqlite3 > node-gyp > tar@2.2.2: This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.\n\u001b[0m" 652 | }, 653 | { 654 | "stream": "\u001b[91mwarning sqlite3 > node-gyp > request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142\n\u001b[0m" 655 | }, 656 | { 657 | "stream": "\u001b[91mwarning sqlite3 > node-gyp > request > har-validator@5.1.5: this library is no longer supported\n\u001b[0m" 658 | }, 659 | { 660 | "stream": "\u001b[91mwarning sqlite3 > node-gyp > request > uuid@3.4.0: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.\n\u001b[0m" 661 | }, 662 | { "stream": "[2/4] Fetching packages...\n" }, 663 | { "stream": "[3/4] Linking dependencies...\n" }, 664 | { "stream": "[4/4] Building fresh packages...\n" }, 665 | { 666 | "stream": "info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.\n" 667 | }, 668 | { 669 | "stream": "\u001b[91merror /app/node_modules/sqlite3: Command failed.\nExit code: 1\nCommand: node-pre-gyp install --fallback-to-build\nArguments: \nDirectory: /app/node_modules/sqlite3\nOutput:\nnode-pre-gyp info it worked if it ends with ok\nnode-pre-gyp info using node-pre-gyp@0.11.0\nnode-pre-gyp info using node@12.22.8 | linux | arm64\nnode-pre-gyp WARN Using request for node-pre-gyp https download \nnode-pre-gyp info check checked for \"/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64/node_sqlite3.node\" (not found)\nnode-pre-gyp http GET https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-arm64.tar.gz\nnode-pre-gyp http 403 https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-arm64.tar.gz\nnode-pre-gyp WARN Tried to download(403): https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-arm64.tar.gz \nnode-pre-gyp WARN Pre-built binaries not found for sqlite3@5.0.2 and node@12.22.8 (node-v72 ABI, musl) (falling back to source compile with node-gyp) \nnode-pre-gyp http 403 status code downloading tarball https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-arm64.tar.gz \ngyp info it worked if it ends with ok\ngyp info using node-gyp@3.8.0\ngyp info using node@12.22.8 | linux | arm64\ngyp info ok \ngyp info it worked if it ends with ok\ngyp info using node-gyp@3.8.0\ngyp info using node@12.22.8 | linux | arm64\ngyp ERR! configure error \ngyp ERR! stack Error: Can't find Python executable \"python\", you can set the PYTHON env variable.\ngyp ERR! stack at PythonFinder.failNoPython (/app/node_modules/node-gyp/lib/configure.js:484:19)\ngyp ERR! stack at PythonFinder. (/app/node_modules/node-gyp/lib/configure.js:406:16)\ngyp ERR! stack at F (/app/node_modules/which/which.js:68:16)\ngyp ERR! stack at E (/app/node_modules/which/which.js:80:29)\ngyp ERR! stack at /app/node_modules/which/which.js:89:16\ngyp ERR! stack at /app/node_modules/isexe/index.js:42:5\ngyp ERR! stack at /app/node_modules/isexe/mode.js:8:5\ngyp ERR! stack at FSReqCallback.oncomplete (fs.js:168:21)\ngyp ERR! System Linux 5.10.76-linuxkit\ngyp ERR! command \"/usr/local/bin/node\" \"/app/node_modules/node-gyp/bin/node-gyp.js\" \"configure\" \"--fallback-to-build\" \"--module=/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64/node_sqlite3.node\" \"--module_name=node_sqlite3\" \"--module_path=/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64\" \"--napi_version=8\" \"--node_abi_napi=napi\" \"--napi_build_version=3\" \"--node_napi_label=napi-v3\"\ngyp ERR! cwd /app/node_modules/sqlite3\ngyp ERR! node -v v12.22.8\ngyp ERR! node-gyp -v v3.8.0\ngyp ERR! not ok \nnode-pre-gyp ERR! build error \nnode-pre-gyp ERR! stack Error: Failed to execute '/usr/local/bin/node /app/node_modules/node-gyp/bin/node-gyp.js configure --fallback-to-build --module=/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64/node_sqlite3.node --module_name=node_sqlite3 --module_path=/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64 --napi_version=8 --node_abi_napi=napi --napi_build_version=3 --node_napi_label=napi-v3' (1)\nnode-pre-gyp ERR! stack at ChildProcess. (/app/node_modules/node-pre-gyp/lib/util/compile.js:83:29)\nnode-pre-gyp ERR! stack at ChildProcess.emit (events.js:314:20)\nnode-pre-gyp ERR! stack at maybeClose (internal/child_process.js:1022:16)\nnode-pre-gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:287:5)\nnode-pre-gyp ERR! System Linux 5.10.76-linuxkit\nnode-pre-gyp ERR! command \"/usr/local/bin/node\" \"/app/node_modules/sqlite3/node_modules/.bin/node-pre-gyp\" \"install\" \"--fallback-to-build\"\nnode-pre-gyp ERR! cwd /app/node_modules/sqlite3\nnode-pre-gyp ERR! node -v v12.22.8\nnode-pre-gyp ERR! node-pre-gyp -v v0.11.0\nnode-pre-gyp ERR! not ok \nFailed to execute '/usr/local/bin/node /app/node_modules/node-gyp/bin/node-gyp.js configure --fallback-to-build --module=/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64/node_sqlite3.node --module_name=node_sqlite3 --module_path=/app/node_modules/sqlite3/lib/binding/napi-v3-linux-arm64 --napi_version=8 --node_abi_napi=napi --napi_build_version=3 --node_napi_label=napi-v3' (1)\n\u001b[0m" 670 | }, 671 | { "stream": "Removing intermediate container 447914b7c511\n" }, 672 | { 673 | "error": "The command '/bin/sh -c yarn install' returned a non-zero code: 1", 674 | "errorDetail": { 675 | "code": 1, 676 | "message": "The command '/bin/sh -c yarn install' returned a non-zero code: 1" 677 | } 678 | }, 679 | "Build done!" 680 | ] 681 | -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext } from "next"; 2 | import { ParsedUrlQuery } from "querystring"; 3 | 4 | export default async function fetchApi(url: string, init?: RequestInit) { 5 | const r = await fetch(process.env.NEXT_PUBLIC_API_BASE + url, { 6 | credentials: "include", 7 | ...init, 8 | }); 9 | if (!r.ok) { 10 | throw { url, resp: r }; 11 | } 12 | 13 | return await r.json(); 14 | } 15 | 16 | // This function properly forwards headers when performing an SSR request 17 | export async function fetchSSR( 18 | url: string, 19 | ctx: GetServerSidePropsContext, 20 | init?: RequestInit 21 | ) { 22 | return await fetchApi(url, { 23 | headers: ctx.req.headers as HeadersInit, 24 | ...init, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/testHelpers.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from "@cypress/react"; 2 | import { ChakraProvider } from "@chakra-ui/react"; 3 | import theme from "../theme"; 4 | 5 | export function mountChakra(node: React.ReactNode) { 6 | mount({node}); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/withAuth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetServerSidePropsContext, 3 | GetServerSidePropsResult, 4 | PreviewData, 5 | } from "next"; 6 | import { ParsedUrlQuery } from "querystring"; 7 | import { IUser } from "../types/haas"; 8 | import { fetchSSR } from "./fetch"; 9 | 10 | export enum OnAuthFailure { 11 | Continue, 12 | RedirectToLogin, 13 | } 14 | 15 | export enum OnOtherFailure { 16 | RedirectToLogin, 17 | ReturnNotFound, 18 | } 19 | 20 | type Reqs = [IUser, ...any[]]; 21 | 22 | export default function withAuth( 23 | next: GetServerSidePropsWithAuth, 24 | otherUrls: string[] = [], 25 | onAuthFailure = OnAuthFailure.RedirectToLogin, 26 | onOtherFailure = OnOtherFailure.ReturnNotFound, 27 | loginRedirectUrl = "/api/login" 28 | ): GetServerSidePropsWithAuth { 29 | const loginRedirect = { 30 | redirect: { destination: loginRedirectUrl, permanent: false }, 31 | }; 32 | return async (ctx) => { 33 | try { 34 | const reqs = (await Promise.all( 35 | ["/users/me", ...otherUrls].map((i) => fetchSSR(i, ctx)) 36 | )) as Reqs; 37 | return await next({ ...ctx, reqs }); 38 | } catch (e) { 39 | if (e.url === "/users/me") { 40 | switch (onAuthFailure) { 41 | case OnAuthFailure.Continue: 42 | return await next(ctx); 43 | case OnAuthFailure.RedirectToLogin: 44 | return loginRedirect; 45 | } 46 | } else { 47 | switch (onOtherFailure) { 48 | case OnOtherFailure.ReturnNotFound: 49 | return { 50 | notFound: true, 51 | }; 52 | case OnOtherFailure.RedirectToLogin: 53 | return loginRedirect; 54 | } 55 | } 56 | } 57 | }; 58 | } 59 | 60 | // the only difference between this and the normal GetServerSideProps type from next is that this uses our custom context type 61 | export type GetServerSidePropsWithAuth< 62 | P extends { [key: string]: any } = { [key: string]: any }, 63 | Q extends ParsedUrlQuery = ParsedUrlQuery, 64 | D extends PreviewData = PreviewData 65 | > = (context: ContextWithAuth) => Promise>; 66 | 67 | // this context type adds a `reqs` property which is an array of the requests made 68 | type ContextWithAuth< 69 | Q extends ParsedUrlQuery = ParsedUrlQuery, 70 | D extends PreviewData = PreviewData 71 | > = GetServerSidePropsContext & { reqs: Reqs }; 72 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@chakra-ui/button"; 2 | import { Heading, Flex } from "@chakra-ui/layout"; 3 | import Icon from "@hackclub/icons"; 4 | import Link from "next/link"; 5 | 6 | export default function NotFound() { 7 | return ( 8 | 14 | Page not found. 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { SWRConfig } from "swr"; 2 | import fetchApi from "../lib/fetch"; 3 | import Router from "next/router"; 4 | import NProgress from "nprogress"; 5 | import { Chakra } from "../components/Chakra"; 6 | 7 | import "@hackclub/theme/fonts/reg-bold.css"; 8 | import "../styles/globals.css"; 9 | import "../styles/nprogress.css"; 10 | 11 | let progressBarTimeout = null; 12 | 13 | const startProgressBar = () => { 14 | clearTimeout(progressBarTimeout); 15 | progressBarTimeout = setTimeout(NProgress.start, 500); 16 | }; 17 | 18 | const stopProgressBar = () => { 19 | clearTimeout(progressBarTimeout); 20 | NProgress.done(); 21 | }; 22 | 23 | NProgress.configure({ 24 | showSpinner: false, 25 | }); 26 | 27 | Router.events.on("routeChangeStart", () => startProgressBar()); 28 | Router.events.on("routeChangeComplete", () => stopProgressBar()); 29 | Router.events.on("routeChangeError", () => stopProgressBar()); 30 | 31 | function MyApp({ Component, pageProps }) { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default MyApp; 42 | -------------------------------------------------------------------------------- /src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Heading, Text } from "@chakra-ui/react"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | 5 | function Error({ statusCode }) { 6 | return ( 7 | <> 8 | 9 | Error {statusCode} | Hack as a Service 10 | 11 | 12 | 13 | 20 | 21 | Error {statusCode} 22 | 23 | 24 | Would you like to go back home? 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | Error.getInitialProps = ({ res, err }) => { 32 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404; 33 | return { statusCode }; 34 | }; 35 | 36 | export default Error; 37 | -------------------------------------------------------------------------------- /src/pages/apps/[id]/addons.tsx: -------------------------------------------------------------------------------- 1 | import AppLayout from "../../../layouts/AppLayout"; 2 | 3 | import { Stat } from "../../../components/Stat"; 4 | import { Addon } from "../../../components/Addon"; 5 | 6 | import { Flex } from "@chakra-ui/react"; 7 | import { devAddons } from "../../../lib/dummyData"; 8 | 9 | import { GetServerSideProps } from "next"; 10 | import { useRouter } from "next/router"; 11 | 12 | import useSWR from "swr"; 13 | 14 | import { fetchSSR } from "../../../lib/fetch"; 15 | import { IApp, ITeam, IUser } from "../../../types/haas"; 16 | import { withCookies } from "../../../components/Chakra"; 17 | export default function AppAddonOverview(props: { 18 | user: IUser; 19 | app: IApp; 20 | team: ITeam; 21 | }) { 22 | const router = useRouter(); 23 | const { id } = router.query; 24 | 25 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 26 | const { data: app } = useSWR(`/apps/${id}`, { fallbackData: props.app }); 27 | const { data: team } = useSWR(() => "/teams/" + app.team_id, { 28 | fallbackData: props.team, 29 | }); 30 | return ( 31 | 32 | 33 | 38 | 43 | 44 | {devAddons.map((addon) => ( 45 | 46 | ))} 47 | 48 | ); 49 | } 50 | 51 | export const getServerSideProps: GetServerSideProps = withCookies( 52 | async (ctx) => { 53 | try { 54 | const [user, app] = await Promise.all( 55 | ["/users/me", `/apps/${ctx.params.id}`].map((i) => fetchSSR(i, ctx)) 56 | ); 57 | 58 | const team = await fetchSSR(`/teams/${app.team_id}`, ctx); 59 | 60 | return { 61 | props: { 62 | user, 63 | app, 64 | team, 65 | }, 66 | }; 67 | } catch (e) { 68 | if (e.url == "/users/me") { 69 | return { 70 | redirect: { 71 | destination: "/api/login", 72 | permanent: false, 73 | }, 74 | }; 75 | } else { 76 | return { 77 | notFound: true, 78 | }; 79 | } 80 | } 81 | } 82 | ); 83 | -------------------------------------------------------------------------------- /src/pages/apps/[id]/deploy.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Input, Text } from "@chakra-ui/react"; 2 | import { GetServerSideProps } from "next"; 3 | import { useRouter } from "next/router"; 4 | import { FormEvent, useRef } from "react"; 5 | import useSWR from "swr"; 6 | import { withCookies } from "../../../components/Chakra"; 7 | import AppLayout from "../../../layouts/AppLayout"; 8 | import fetchApi, { fetchSSR } from "../../../lib/fetch"; 9 | import { IApp, ITeam, IUser } from "../../../types/haas"; 10 | 11 | export default function AppDeployPage(props: { 12 | user: IUser; 13 | app: IApp; 14 | team: ITeam; 15 | }) { 16 | const router = useRouter(); 17 | const { id } = router.query; 18 | 19 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 20 | const { data: app } = useSWR(`/apps/${id}`, { fallbackData: props.app }); 21 | const { data: team } = useSWR(() => "/teams/" + app.team_id, { 22 | fallbackData: props.team, 23 | }); 24 | 25 | const repoUrlRef = useRef(null); 26 | 27 | async function onSubmit(e: FormEvent) { 28 | e.preventDefault(); 29 | const url = repoUrlRef.current.value; 30 | const res = await fetchApi(`/apps/${id}/deploy`, { 31 | method: "POST", 32 | body: JSON.stringify({ 33 | git_repository: url, 34 | }), 35 | }); 36 | router.push(`/builds/${res.id}`); 37 | } 38 | 39 | return ( 40 | 41 | 42 | 43 | Git repository URL 44 |
45 | 46 | Must be a public repository 47 | 48 |
49 | 50 | 53 |
54 |
55 | ); 56 | } 57 | 58 | export const getServerSideProps: GetServerSideProps = withCookies( 59 | async (ctx) => { 60 | try { 61 | const [user, app] = await Promise.all( 62 | ["/users/me", `/apps/${ctx.params.id}`].map((i) => fetchSSR(i, ctx)) 63 | ); 64 | 65 | const team = await fetchSSR(`/teams/${app.team_id}`, ctx); 66 | 67 | return { 68 | props: { 69 | user, 70 | app, 71 | team, 72 | }, 73 | }; 74 | } catch (e) { 75 | if (e.url == "/users/me") { 76 | return { 77 | redirect: { 78 | destination: "/api/login", 79 | permanent: false, 80 | }, 81 | }; 82 | } else { 83 | return { 84 | notFound: true, 85 | }; 86 | } 87 | } 88 | } 89 | ); 90 | -------------------------------------------------------------------------------- /src/pages/apps/[id]/domains.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Flex, 4 | FormControl, 5 | FormErrorMessage, 6 | Input, 7 | useColorMode, 8 | } from "@chakra-ui/react"; 9 | import { Formik, FormikHelpers } from "formik"; 10 | import { GetServerSideProps } from "next"; 11 | import Head from "next/head"; 12 | import { useRouter } from "next/router"; 13 | import React from "react"; 14 | import useSWR from "swr"; 15 | import { withCookies } from "../../../components/Chakra"; 16 | import Domain from "../../../components/Domain"; 17 | import AppLayout from "../../../layouts/AppLayout"; 18 | import fetchApi, { fetchSSR } from "../../../lib/fetch"; 19 | import { IApp, IDomain, ITeam, IUser } from "../../../types/haas"; 20 | 21 | function AddDomainForm({ 22 | onSubmit, 23 | }: { 24 | onSubmit: ( 25 | values: { domain: string }, 26 | formikHelpers: FormikHelpers<{ domain: string }> 27 | ) => void | Promise; 28 | }) { 29 | const { colorMode } = useColorMode(); 30 | 31 | return ( 32 | { 35 | if (!domain) { 36 | return { domain: undefined }; 37 | } 38 | 39 | if (!/^([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,}$/.test(domain)) { 40 | return { 41 | domain: "Invalid domain.", 42 | }; 43 | } 44 | 45 | return {}; 46 | }} 47 | onSubmit={onSubmit} 48 | validateOnChange={false} 49 | > 50 | {({ 51 | handleSubmit, 52 | handleChange, 53 | handleBlur, 54 | values, 55 | isSubmitting, 56 | errors, 57 | }) => ( 58 |
59 | 60 | 61 | 74 | 75 | {errors.domain} 76 | 77 | 78 | 79 | 88 | 89 |
90 | )} 91 |
92 | ); 93 | } 94 | 95 | export default function DomainsPage(props: { 96 | user: IUser; 97 | app: IApp; 98 | team: ITeam; 99 | domains: IDomain[]; 100 | }) { 101 | const router = useRouter(); 102 | const { id } = router.query; 103 | 104 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 105 | const { data: app } = useSWR(`/apps/${id}`, { fallbackData: props.app }); 106 | const { data: team } = useSWR(`/teams/${app.team_id}`, { 107 | fallbackData: props.team, 108 | }); 109 | const { data: domains, mutate: mutateDomains } = useSWR( 110 | `/apps/${id}/domains`, 111 | { 112 | fallbackData: props.domains, 113 | } 114 | ); 115 | 116 | return ( 117 | 118 | 119 | {app.slug} - Domains 120 | 121 | 122 | { 124 | try { 125 | const domain = await fetchApi(`/apps/${app.slug}/domains`, { 126 | method: "POST", 127 | body: JSON.stringify({ domain: values.domain }), 128 | }); 129 | 130 | mutateDomains([...domains, domain], false); 131 | resetForm(); 132 | } catch (e) { 133 | if (e.resp?.status === 409) { 134 | setErrors({ 135 | domain: "This domain is already in use.", 136 | }); 137 | } else { 138 | setErrors({ domain: "An unknown error occurred." }); 139 | } 140 | } 141 | 142 | setSubmitting(false); 143 | }} 144 | /> 145 | 146 | {domains.map((d) => { 147 | return ; 148 | })} 149 | 150 | ); 151 | } 152 | 153 | export const getServerSideProps: GetServerSideProps = withCookies( 154 | async (ctx) => { 155 | try { 156 | const [user, app, domains] = await Promise.all( 157 | [ 158 | "/users/me", 159 | `/apps/${ctx.params.id}`, 160 | `/apps/${ctx.params.id}/domains`, 161 | ].map((i) => fetchSSR(i, ctx)) 162 | ); 163 | const team = await fetchSSR(`/teams/${app.team_id}`, ctx); 164 | 165 | return { 166 | props: { 167 | user, 168 | app, 169 | team, 170 | domains, 171 | }, 172 | }; 173 | } catch (e) { 174 | if (e.url == "/users/me") { 175 | return { 176 | redirect: { 177 | destination: "/api/login", 178 | permanent: false, 179 | }, 180 | }; 181 | } else { 182 | return { 183 | notFound: true, 184 | }; 185 | } 186 | } 187 | } 188 | ); 189 | -------------------------------------------------------------------------------- /src/pages/apps/[id]/environment.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from "next"; 2 | import { useRouter } from "next/router"; 3 | import useSWR from "swr"; 4 | import AppLayout from "../../../layouts/AppLayout"; 5 | import { fetchSSR } from "../../../lib/fetch"; 6 | import { Button, Flex, IconButton, Input, ButtonGroup } from "@chakra-ui/react"; 7 | import { IApp, ITeam, IUser } from "../../../types/haas"; 8 | import React, { useState } from "react"; 9 | import Icon from "@hackclub/icons"; 10 | import { withCookies } from "../../../components/Chakra"; 11 | 12 | function EnvVar({ 13 | envVar, 14 | value, 15 | onRemove, 16 | onKeyChange, 17 | onValueChange, 18 | disabled, 19 | }: { 20 | envVar: string; 21 | value: string; 22 | onRemove: () => void; 23 | onKeyChange: (key: string) => void; 24 | onValueChange: (value: string) => void; 25 | disabled?: boolean; 26 | }) { 27 | return ( 28 | 29 | onKeyChange(e.target.value)} 35 | disabled={disabled} 36 | fontFamily="mono" 37 | /> 38 | onValueChange(e.target.value)} 44 | disabled={disabled} 45 | fontFamily="mono" 46 | /> 47 | } 50 | disabled={disabled} 51 | onClick={() => onRemove()} 52 | /> 53 | 54 | ); 55 | } 56 | 57 | export default function EnvironmentPage(props: { 58 | user: IUser; 59 | app: IApp; 60 | team: ITeam; 61 | }) { 62 | const router = useRouter(); 63 | const { id } = router.query; 64 | 65 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 66 | const { data: app } = useSWR(`/apps/${id}`, { fallbackData: props.app }); 67 | const { data: team } = useSWR(() => "/teams/" + app.team_id, { 68 | fallbackData: props.team, 69 | }); 70 | 71 | const [env, setEnv] = useState<{ key: string; value: string; id: string }[]>( 72 | [] 73 | ); 74 | const [loading] = useState(null); 75 | 76 | return ( 77 | 78 | 79 | 89 | 92 | 93 | 94 | {env.map(({ key, value, id }) => ( 95 | { 101 | setEnv( 102 | env.map((x) => { 103 | if (x.id == id) { 104 | x.key = e; 105 | } 106 | 107 | return x; 108 | }) 109 | ); 110 | }} 111 | onValueChange={(e) => { 112 | setEnv( 113 | env.map((x) => { 114 | if (x.id == id) { 115 | x.value = e; 116 | } 117 | 118 | return x; 119 | }) 120 | ); 121 | }} 122 | onRemove={() => { 123 | setEnv((e) => e.filter((x) => x.id != id)); 124 | }} 125 | /> 126 | ))} 127 | 128 | ); 129 | } 130 | 131 | export const getServerSideProps: GetServerSideProps = withCookies( 132 | async (ctx) => { 133 | try { 134 | const [user, app] = await Promise.all( 135 | ["/users/me", `/apps/${ctx.params.id}`].map((i) => fetchSSR(i, ctx)) 136 | ); 137 | 138 | const team = await fetchSSR(`/teams/${app.team_id}`, ctx); 139 | 140 | return { 141 | props: { 142 | user, 143 | app, 144 | team, 145 | }, 146 | }; 147 | } catch (e) { 148 | if (e.url == "/users/me") { 149 | return { 150 | redirect: { 151 | destination: "/api/login", 152 | permanent: false, 153 | }, 154 | }; 155 | } else { 156 | return { 157 | notFound: true, 158 | }; 159 | } 160 | } 161 | } 162 | ); 163 | -------------------------------------------------------------------------------- /src/pages/apps/[id]/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect, useState } from "react"; 3 | import AppLayout from "../../../layouts/AppLayout"; 4 | import { Text, useColorMode } from "@chakra-ui/react"; 5 | import Logs from "../../../components/Logs"; 6 | import { GetServerSideProps } from "next"; 7 | import { fetchSSR } from "../../../lib/fetch"; 8 | import { IApp, ITeam, IUser } from "../../../types/haas"; 9 | import useSWR from "swr"; 10 | import Ansi from "ansi-to-react"; 11 | import { withCookies } from "../../../components/Chakra"; 12 | 13 | interface ILog { 14 | stream: "stdout" | "stderr"; 15 | log: string; 16 | } 17 | 18 | function useLogs(appId: string): { logs: ILog[]; error: string | undefined } { 19 | const [logs, setLogs] = useState([]); 20 | 21 | useEffect(() => { 22 | if (!appId) return; 23 | 24 | const ws = new WebSocket( 25 | `${process.env.NEXT_PUBLIC_API_BASE.replace( 26 | "http", 27 | "ws" 28 | )}/apps/${appId}/logs` 29 | ); 30 | 31 | ws.onopen = () => { 32 | setLogs([]); 33 | }; 34 | 35 | ws.onmessage = (e) => { 36 | setLogs((old) => old.concat(JSON.parse(e.data))); 37 | }; 38 | 39 | return () => { 40 | ws.close(); 41 | }; 42 | }, [appId]); 43 | 44 | return { logs, error: undefined }; 45 | } 46 | 47 | export default function AppDashboardPage(props: { 48 | user: IUser; 49 | app: IApp; 50 | team: ITeam; 51 | }) { 52 | const router = useRouter(); 53 | const { id } = router.query; 54 | 55 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 56 | const { data: app } = useSWR(`/apps/${id}`, { fallbackData: props.app }); 57 | const { data: team } = useSWR(() => "/teams/" + app.team_id, { 58 | fallbackData: props.team, 59 | }); 60 | 61 | const { colorMode } = useColorMode(); 62 | 63 | const { logs } = useLogs(id as string); 64 | 65 | return ( 66 | 67 | log.log} 70 | render={(i) => ( 71 | <> 72 | 77 | [{i.stream}] 78 | {" "} 79 | 84 | {i.log} 85 | 86 | 87 | )} 88 | /> 89 | 90 | ); 91 | } 92 | 93 | export const getServerSideProps: GetServerSideProps = withCookies( 94 | async (ctx) => { 95 | try { 96 | const [user, app] = await Promise.all( 97 | ["/users/me", `/apps/${ctx.params.id}`].map((i) => fetchSSR(i, ctx)) 98 | ); 99 | 100 | const team = await fetchSSR(`/teams/${app.team_id}`, ctx); 101 | 102 | return { 103 | props: { 104 | user, 105 | app, 106 | team, 107 | }, 108 | }; 109 | } catch (e) { 110 | if (e.url == "/users/me") { 111 | return { 112 | redirect: { 113 | destination: "/api/login", 114 | permanent: false, 115 | }, 116 | }; 117 | } else { 118 | return { 119 | notFound: true, 120 | }; 121 | } 122 | } 123 | } 124 | ); 125 | -------------------------------------------------------------------------------- /src/pages/auth/device.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Flex, 4 | Heading, 5 | HStack, 6 | PinInput, 7 | PinInputField, 8 | useToast, 9 | Button, 10 | } from "@chakra-ui/react"; 11 | import Icon from "@hackclub/icons"; 12 | import { GetServerSideProps } from "next"; 13 | import Head from "next/head"; 14 | import React, { useRef, useState } from "react"; 15 | import { ArrowRight } from "react-feather"; 16 | import { withCookies } from "../../components/Chakra"; 17 | import { fetchSSR } from "../../lib/fetch"; 18 | 19 | export default function CLIAuthPage() { 20 | const toast = useToast(); 21 | 22 | const [appName, setAppName] = useState(undefined); 23 | const code = useRef(undefined); 24 | const submitButton = useRef(null); 25 | 26 | const [pageState, setPageState] = useState( 27 | undefined 28 | ); 29 | 30 | return ( 31 | 37 | 38 | Device Authentication 39 | 40 | 41 | 49 | {pageState === "success" ? ( 50 | 51 | ) : ( 52 | 53 | )} 54 | 55 | {appName && ( 56 | 57 | Log in to{" "} 58 | 64 | {appName} 65 | 66 | 67 | )} 68 | 69 | 70 | {pageState === "success" 71 | ? "You've been logged in!" 72 | : "Enter the code displayed on your device."} 73 | 74 | 75 | {pageState !== "success" && ( 76 |
{ 78 | e.preventDefault(); 79 | 80 | setPageState("loading"); 81 | 82 | try { 83 | const r = await fetch( 84 | process.env.NEXT_PUBLIC_API_BASE + 85 | `/oauth/device_authorizations/${code.current}/approve`, 86 | { 87 | method: "POST", 88 | } 89 | ); 90 | if (!r.ok) { 91 | throw { resp: r }; 92 | } 93 | 94 | setPageState("success"); 95 | } catch (e) { 96 | toast({ 97 | title: "Something went wrong.", 98 | status: "error", 99 | position: "top", 100 | }); 101 | 102 | setPageState(undefined); 103 | } 104 | }} 105 | > 106 | 107 | 108 | { 113 | try { 114 | const r = await fetch( 115 | process.env.NEXT_PUBLIC_API_BASE + 116 | `/oauth/device_authorizations/${value}/app_name` 117 | ); 118 | if (!r.ok) { 119 | throw { resp: r }; 120 | } 121 | 122 | setAppName(await r.text()); 123 | 124 | code.current = value; 125 | 126 | submitButton.current.focus(); 127 | } catch (e) { 128 | toast({ 129 | title: "Code is invalid or expired.", 130 | status: "error", 131 | position: "top", 132 | }); 133 | setAppName(undefined); 134 | } 135 | }} 136 | > 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 157 | 158 |
159 | )} 160 |
161 | ); 162 | } 163 | 164 | export const getServerSideProps: GetServerSideProps = withCookies( 165 | async (ctx) => { 166 | try { 167 | await fetchSSR("/users/me", ctx); 168 | 169 | return { 170 | props: {}, 171 | }; 172 | } catch (e) { 173 | console.log(e); 174 | 175 | return { 176 | redirect: { 177 | destination: "/api/login?return_to=/auth/device", 178 | permanent: false, 179 | }, 180 | }; 181 | } 182 | } 183 | ); 184 | -------------------------------------------------------------------------------- /src/pages/beep.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@chakra-ui/react"; 2 | import { GetServerSideProps } from "next"; 3 | import useSWR from "swr"; 4 | import { withCookies } from "../components/Chakra"; 5 | import HaasLayout, { 6 | SidebarBackButton, 7 | SidebarHeader, 8 | SidebarItem, 9 | } from "../layouts/HaasLayout"; 10 | import { fetchSSR } from "../lib/fetch"; 11 | import { IUser } from "../types/haas"; 12 | 13 | export default function BeepPage(props: { user: IUser }) { 14 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 15 | 16 | return ( 17 | 22 | 23 | 24 | 25 | Hack Club HQ 26 | 27 | 28 | 29 | Apps 7 30 | 31 | 32 | People 33 | 34 | 35 | Settings 36 | 37 | 38 | } 39 | > 40 | yeah 41 | 42 | ); 43 | } 44 | 45 | export const getServerSideProps: GetServerSideProps = withCookies( 46 | async (ctx) => { 47 | try { 48 | const user = await fetchSSR("/users/me", ctx); 49 | 50 | return { 51 | props: { 52 | user, 53 | }, 54 | }; 55 | } catch (e) { 56 | return { 57 | redirect: { 58 | destination: "/api/login", 59 | permanent: false, 60 | }, 61 | }; 62 | } 63 | } 64 | ); 65 | -------------------------------------------------------------------------------- /src/pages/builds/[id].tsx: -------------------------------------------------------------------------------- 1 | // TODO: toggles for auto scroll/word wrap, display other events, connect to build endpoints + sse 2 | import { useRouter } from "next/router"; 3 | import { Fragment, useEffect, useState, useRef, CSSProperties } from "react"; 4 | import useSWR from "swr"; 5 | import { 6 | Accordion, 7 | AccordionButton, 8 | AccordionIcon, 9 | AccordionItem, 10 | AccordionPanel, 11 | useAccordionItemState, 12 | Box, 13 | Flex, 14 | Progress, 15 | Text, 16 | useColorMode, 17 | Heading, 18 | } from "@chakra-ui/react"; 19 | import HaasLayout, { SidebarBackButton } from "../../layouts/HaasLayout"; 20 | import { GetServerSideProps } from "next"; 21 | import { fetchSSR } from "../../lib/fetch"; 22 | import { IApp, IBuild, IUser } from "../../types/haas"; 23 | import Ansi from "ansi-to-react"; 24 | import successLogs from "../../lib/successBuildEventLogs.json"; 25 | import prettyBytes from "pretty-bytes"; 26 | import Head from "next/head"; 27 | import { IDockerBuildEvent, IPayload } from "../../types/build"; 28 | import { withCookies } from "../../components/Chakra"; 29 | 30 | type TDockerBuildEvent = IDockerBuildEvent & { ts: string }; 31 | 32 | interface IBuildStepData { 33 | stepLog: TDockerBuildEvent; 34 | beginContainerId?: string; 35 | endContainerId?: string; 36 | progressLogs: Map< 37 | string, 38 | TDockerBuildEvent & Required> 39 | >; 40 | logs: TDockerBuildEvent[]; 41 | } 42 | 43 | function parseEvent(s: string): IPayload { 44 | const event = JSON.parse(s); 45 | event.date = new Date(event.ts / 1000000); 46 | return event; 47 | } 48 | 49 | const buildLogAsString = (log: IDockerBuildEvent) => { 50 | let val = ""; 51 | if (log?.stream) val += log.stream; 52 | if (log?.status) val += log.status; 53 | return val === "" ? null : val; 54 | }; 55 | 56 | export default function BuildPage(props: { 57 | user: IUser; 58 | build: IBuild; 59 | app: IApp; 60 | }) { 61 | const router = useRouter(); 62 | const { id } = router.query; 63 | 64 | const { data: build, mutate: mutateBuild } = useSWR(`/builds/${id}`, { 65 | fallbackData: props.build, 66 | }); 67 | const { data: app } = useSWR(() => "/apps/" + build?.app_id, { 68 | fallbackData: props.app, 69 | }); 70 | 71 | const [autoScroll, setAutoScroll] = useState(true); 72 | const [wordWrap, setWordWrap] = useState(true); 73 | 74 | const wrapStyle: CSSProperties = wordWrap 75 | ? { 76 | whiteSpace: "pre-wrap", 77 | wordWrap: "break-word", 78 | } 79 | : {}; 80 | 81 | const logsElement = useRef(null); 82 | const [cloneLogs, setCloneLogs] = useState([]); 83 | const [buildLogs, setBuildLogs] = useState([]); 84 | const [deployLogs, setDeployLogs] = useState([]); 85 | const [error, setError] = useState(null); 86 | const { colorMode } = useColorMode(); 87 | useEffect(() => { 88 | if (autoScroll && logsElement.current) { 89 | logsElement.current.scrollIntoView(false); 90 | } 91 | }, [buildLogs]); 92 | 93 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 94 | 95 | function processEvent(event: IPayload) { 96 | if ("Err" in event) { 97 | setError(event.Err); 98 | } else { 99 | const event2 = event.Ok; 100 | switch (event2.type) { 101 | case "git_clone": 102 | // XXX: for some reason this variable is needed for TS to infer types properly 103 | const cloneEv = event2.event; 104 | setCloneLogs((prev) => prev.concat(cloneEv)); 105 | break; 106 | case "docker_build": 107 | const buildLog = event2.event; 108 | setBuildLogs((prev) => { 109 | const newArr = [...prev]; 110 | if (buildLog.stream === "\n") return newArr; 111 | if (buildLog.stream?.startsWith("Step")) { 112 | if ( 113 | newArr[newArr.length - 1]?.stepLog?.stream === 114 | "Waiting for data..." 115 | ) { 116 | newArr[newArr.length - 1].stepLog = { 117 | ...buildLog, 118 | ts: event.ts, 119 | }; 120 | } else { 121 | newArr.push({ 122 | stepLog: { ...buildLog, ts: event.ts }, 123 | progressLogs: new Map(), 124 | logs: [], 125 | }); 126 | } 127 | return newArr; 128 | } 129 | if (newArr.length < 1) { 130 | // FIXME - should never happen? 131 | newArr.push({ 132 | stepLog: { 133 | stream: "Waiting for data...", 134 | ts: Date.now().toString(), 135 | }, 136 | progressLogs: new Map(), 137 | logs: [], 138 | }); 139 | } 140 | const newStep = newArr[newArr.length - 1]; 141 | const beginContainerIdRegex = /^ ---> Running in (\S+)\s*$/; 142 | if (beginContainerIdRegex.test(buildLog.stream)) { 143 | const containerId = beginContainerIdRegex.exec( 144 | buildLog.stream 145 | )[1]; 146 | newStep.beginContainerId = containerId; 147 | } 148 | const endContainerIdRegex = /^ ---> (\S+)\s*$/; 149 | if (endContainerIdRegex.test(buildLog.stream)) { 150 | const containerId = endContainerIdRegex.exec(buildLog.stream)[1]; 151 | newStep.endContainerId = containerId; 152 | } 153 | if (buildLog.id) { 154 | newStep.progressLogs.set(buildLog.id, { 155 | ...buildLog, 156 | ts: event.ts, 157 | } as any); 158 | } else { 159 | newStep.logs.push({ ...buildLog, ts: event.ts }); 160 | } 161 | newArr[newArr.length - 1] = newStep; 162 | return newArr; 163 | }); 164 | break; 165 | case "deploy": 166 | // TODO: prettify the event 167 | const deployEv = event2.event; 168 | setDeployLogs((logs) => logs.concat([deployEv])); 169 | break; 170 | } 171 | } 172 | } 173 | 174 | useEffect(() => { 175 | setCloneLogs([]); 176 | setBuildLogs([]); 177 | setDeployLogs([]); 178 | const events: IPayload[] = build.events.map((x) => JSON.parse(x)); 179 | events.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime()); 180 | events.forEach(processEvent); 181 | 182 | // replay 1 event / 10 ms 183 | //let currentIdx = 0; 184 | //const pushNewLog = () => { 185 | // let event = events[currentIdx++]; 186 | // //console.log(event); 187 | // processEvent(event); 188 | // if (currentIdx >= events.length) clearInterval(i); 189 | //}; 190 | //const i = setInterval(pushNewLog, 10); 191 | //return () => clearInterval(i); 192 | 193 | // replay events with real relative offset 194 | //const beginTs = new Date(events[0].ts); 195 | //let timeouts = []; 196 | //for (const event of events) { 197 | // const offset = new Date(event.ts).getTime() - beginTs.getTime(); 198 | // timeouts.push(setTimeout(() => processEvent(event), offset)); 199 | //} 200 | //return () => timeouts.forEach(clearTimeout); 201 | }, [build]); 202 | 203 | // until we get sse 204 | useEffect(() => { 205 | const i = setInterval(() => mutateBuild(), 300); 206 | return () => clearInterval(i); 207 | }, []); 208 | 209 | // FIXME: use the Logs component 210 | return ( 211 | {app && }} 216 | > 217 | <> 218 | {/* TODO: fix the accordion thingy, both levels */} 219 | 220 | {`Build ${build.id} for app ${app.slug}`} 221 | 222 | 223 | 224 | 225 | 226 | 227 | Clone 228 | 229 | 230 | 231 | 232 | 233 | 238 | {cloneLogs.map((log) => ( 239 |
249 | 										{log}
250 | 									
251 | ))} 252 |
253 |
254 |
255 | 256 | 257 | 258 | 259 | Build 260 | 261 | 262 | 263 | 264 | 265 | 270 | {buildLogs.map((data, idx) => ( 271 | 272 | 277 | 278 | ))} 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | Deploy 287 | 288 | 289 | 290 | 291 | 292 | 297 | {deployLogs.map((log) => ( 298 |
308 | 										{log}
309 | 									
310 | ))} 311 |
312 |
313 |
314 | {error && Error: {error}} 315 |
316 | 317 |
318 | ); 319 | } 320 | 321 | function BuildStep({ 322 | data, 323 | isLast, 324 | wrapStyle, 325 | }: { 326 | data: IBuildStepData; 327 | isLast: boolean; 328 | wrapStyle: CSSProperties; 329 | }) { 330 | const stepDetails = data.stepLog; 331 | const yeah = useAccordionItemState(); 332 | const { colorMode } = useColorMode(); 333 | useEffect(() => { 334 | if (isLast) { 335 | yeah.onOpen(); 336 | } else { 337 | yeah.onClose(); 338 | } 339 | }, [data, isLast]); 340 | return ( 341 | <> 342 | 343 | 344 | {/* TODO: Maybe add a little indicator icon for step status (loading, checkmark, cross) */} 345 | 346 | {stepDetails.stream} 347 | 348 | 349 | 350 | {data.beginContainerId || ( 351 | <> 352 |              353 | 354 | )} 355 | 356 | {(data.beginContainerId || data.endContainerId) && <>→} 357 | 358 | {data.endContainerId || ( 359 | <> 360 |              361 | 362 | )} 363 | 364 | 365 | 366 | 367 | 368 | 369 | 374 | {Array.from(data.progressLogs.values()).map((ev) => ( 375 | 376 |
385 | 								{`${ev.id}: ${buildLogAsString(ev)}`}
386 | 								{ev.progressDetail &&
387 | 									ev.progressDetail?.current &&
388 | 									ev.progressDetail?.total && (
389 | 										
390 | 											
391 | 												→ {prettyBytes(ev.progressDetail.current)}
392 | 											
393 | 405 |
406 | 												{prettyBytes(ev.progressDetail.total)}
407 | 											
408 |
409 | )} 410 |
411 |
412 | ))} 413 | {data.logs.map((ev) => ( 414 | 415 | {/* TODO: deduplicate this with above section */} 416 |
425 | 								{buildLogAsString(ev)}
426 | 							
427 |
428 | ))} 429 |
430 |
431 | 432 | ); 433 | } 434 | 435 | export const getServerSideProps: GetServerSideProps = withCookies( 436 | async (ctx) => { 437 | try { 438 | const [user, build] = await Promise.all( 439 | ["/users/me", `/builds/${ctx.params.id}`].map((i) => fetchSSR(i, ctx)) 440 | ); 441 | 442 | const app = await fetchSSR(`/apps/${build.app_id}`, ctx); 443 | 444 | return { 445 | props: { 446 | user, 447 | build, 448 | app, 449 | }, 450 | }; 451 | } catch (e) { 452 | if (e.url == "/users/me") { 453 | return { 454 | redirect: { 455 | destination: "/api/login", 456 | permanent: false, 457 | }, 458 | }; 459 | } else { 460 | return { 461 | notFound: true, 462 | }; 463 | } 464 | } 465 | } 466 | ); 467 | -------------------------------------------------------------------------------- /src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { Heading, IconButton, useDisclosure, Grid } from "@chakra-ui/react"; 3 | import App from "../components/App"; 4 | import { GetServerSideProps } from "next"; 5 | import fetchApi, { fetchSSR } from "../lib/fetch"; 6 | import { IApp, ITeam, IUser } from "../types/haas"; 7 | import Head from "next/head"; 8 | import Icon from "@hackclub/icons"; 9 | import AppCreateModal from "../components/AppCreateModal"; 10 | import { useRouter } from "next/router"; 11 | import DashboardLayout from "../layouts/DashboardLayout"; 12 | import { withCookies } from "../components/Chakra"; 13 | 14 | export default function Dashboard(props: { 15 | user: IUser; 16 | teams: ITeam[]; 17 | personalApps: IApp[]; 18 | }) { 19 | const { data: teams } = useSWR("/users/me/teams", { 20 | fallbackData: props.teams, 21 | }); 22 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 23 | const { data: personalApps, mutate: mutatePersonalApps } = useSWR( 24 | `/teams/${teams.find((t) => t.personal).id}/apps`, 25 | { fallbackData: props.personalApps } 26 | ); 27 | 28 | const appModal = useDisclosure(); 29 | 30 | const router = useRouter(); 31 | 32 | return ( 33 | <> 34 | 35 | Personal Apps 36 | 37 | 38 | 49 | 50 | 51 | } 52 | > 53 | { 57 | try { 58 | const app: IApp = await fetchApi( 59 | `/teams/${teams.find((t) => t.personal).slug}/apps`, 60 | { 61 | headers: { 62 | "Content-Type": "application/json", 63 | }, 64 | method: "POST", 65 | body: JSON.stringify({ slug: v.slug }), 66 | } 67 | ); 68 | 69 | mutatePersonalApps([...personalApps, app], false); 70 | 71 | appModal.onClose(); 72 | 73 | router.push(`/apps/${v.slug}/deploy`); 74 | } catch (e) { 75 | if (e.resp?.status === 409) { 76 | setErrors({ 77 | slug: "This name is already taken by another app.", 78 | }); 79 | } 80 | } 81 | 82 | setSubmitting(false); 83 | }} 84 | /> 85 | 86 | {personalApps.length > 0 ? ( 87 | 93 | {personalApps.map((app: IApp) => { 94 | return ( 95 | 101 | ); 102 | })} 103 | 104 | ) : ( 105 | 106 | You don't have any personal apps quite yet. 😢 107 | 108 | )} 109 | 110 | 111 | ); 112 | } 113 | 114 | export const getServerSideProps: GetServerSideProps = withCookies( 115 | async (ctx) => { 116 | try { 117 | const [user, teams] = await Promise.all( 118 | ["/users/me", "/users/me/teams"].map((i) => fetchSSR(i, ctx)) 119 | ); 120 | 121 | const personalApps: IApp[] = await fetchSSR( 122 | `/teams/${teams.find((t) => t.personal).id}/apps`, 123 | ctx 124 | ); 125 | 126 | return { 127 | props: { 128 | user, 129 | teams, 130 | personalApps, 131 | }, 132 | }; 133 | } catch (e) { 134 | return { 135 | redirect: { 136 | destination: "/api/login", 137 | permanent: false, 138 | }, 139 | }; 140 | } 141 | } 142 | ); 143 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Heading, Text, Link as ChakraLink } from "@chakra-ui/react"; 2 | import { GetServerSideProps } from "next"; 3 | import Head from "next/head"; 4 | import Link from "next/link"; 5 | import { fetchSSR } from "../lib/fetch"; 6 | import { withCookies } from "../components/Chakra"; 7 | export default function Home() { 8 | return ( 9 | <> 10 | 11 | Hack as a Service 12 | 13 | 14 | 15 | 22 | 23 | Coming Soon 24 | 25 | 26 | Got early access?{" "} 27 | 28 | Log in. 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export const getServerSideProps: GetServerSideProps = withCookies( 37 | async (ctx) => { 38 | try { 39 | await fetchSSR("/users/me", ctx); 40 | 41 | return { 42 | redirect: { 43 | destination: "/dashboard", 44 | permanent: false, 45 | }, 46 | }; 47 | } catch (e) { 48 | return { 49 | props: { 50 | user: null, 51 | }, 52 | }; 53 | } 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /src/pages/landing.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Flex, Image, Text } from "@chakra-ui/react"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | 5 | export default function Home() { 6 | return ( 7 | 8 | 9 | Hack as a Service 10 | 11 | 12 | 20 | 21 | 22 | 25 | 26 | 27 | 36 | 37 | A managed platform for makers. 38 | 39 | 40 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | import { Heading, Button, Text, Box, Grid } from "@chakra-ui/react"; 3 | 4 | import { GetServerSideProps } from "next"; 5 | import { fetchSSR } from "../lib/fetch"; 6 | import { IApp, ITeam, IUser } from "../types/haas"; 7 | import Head from "next/head"; 8 | import React from "react"; 9 | import DashboardLayout from "../layouts/DashboardLayout"; 10 | import { withCookies } from "../components/Chakra"; 11 | 12 | export default function Settings(props: { 13 | user: IUser; 14 | teams: ITeam[]; 15 | personalApps: IApp[]; 16 | }) { 17 | const { data: teams } = useSWR("/users/me/teams", { 18 | fallbackData: props.teams, 19 | }); 20 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 21 | const { data: personalApps } = useSWR( 22 | `/teams/${teams.find((t) => t.personal).id}/apps`, 23 | { fallbackData: props.personalApps } 24 | ); 25 | 26 | return ( 27 | <> 28 | 29 | Settings 30 | 31 | 32 | 38 | 39 | 40 | 41 | Delete 42 | 43 | Warning: This will: 44 | 45 |
    46 |
  • remove you from all your teams
  • 47 |
  • delete any teams where you are the only user
  • 48 |
  • 49 | 50 | shut down and delete all apps associated with those teams, 51 | 52 | including addons, domains, and other data that may be stored 53 |
  • 54 |
55 |
56 | 57 | You will be asked to confirm this action in order to proceed. 58 | 59 | 62 |
63 | 64 | 65 | Export 66 | 67 | 68 | Download all your user data as JSON. This may take a while to 69 | aggregate - we'll send you a Slack DM when your download is 70 | ready. 71 | 72 | 73 | 74 |
75 |
76 | 77 | ); 78 | } 79 | 80 | export const getServerSideProps: GetServerSideProps = withCookies( 81 | async (ctx) => { 82 | try { 83 | const [user, teams] = await Promise.all( 84 | ["/users/me", "/users/me/teams"].map((i) => fetchSSR(i, ctx)) 85 | ); 86 | 87 | const personalApps: IApp[] = await fetchSSR( 88 | `/teams/${teams.find((t) => t.personal).id}/apps`, 89 | ctx 90 | ); 91 | 92 | return { 93 | props: { 94 | user, 95 | teams, 96 | personalApps, 97 | }, 98 | }; 99 | } catch (e) { 100 | return { 101 | redirect: { 102 | destination: "/api/login", 103 | permanent: false, 104 | }, 105 | }; 106 | } 107 | } 108 | ); 109 | -------------------------------------------------------------------------------- /src/pages/teams/[id]/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import useSWR from "swr"; 3 | import fetchApi, { fetchSSR } from "../../../lib/fetch"; 4 | import { 5 | Box, 6 | Grid, 7 | IconButton, 8 | Tooltip, 9 | useDisclosure, 10 | } from "@chakra-ui/react"; 11 | 12 | import { GetServerSideProps } from "next"; 13 | import { IApp, ITeam, IUser } from "../../../types/haas"; 14 | import TeamLayout from "../../../layouts/TeamLayout"; 15 | import React from "react"; 16 | import App from "../../../components/App"; 17 | import Head from "next/head"; 18 | import Icon from "@hackclub/icons"; 19 | import AppCreateModal from "../../../components/AppCreateModal"; 20 | import { withCookies } from "../../../components/Chakra"; 21 | 22 | export default function TeamPage(props: { 23 | user: IUser; 24 | users: IUser[]; 25 | team: ITeam; 26 | apps: IApp[]; 27 | }) { 28 | const router = useRouter(); 29 | const { id } = router.query; 30 | 31 | const { data: team } = useSWR(`/teams/${id}`, { 32 | fallbackData: props.team, 33 | }); 34 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 35 | const { data: users } = useSWR(`/teams/${id}/users`, { 36 | fallbackData: props.users, 37 | }); 38 | const { data: apps, mutate: mutateApps } = useSWR(`/teams/${id}/apps`, { 39 | fallbackData: props.apps, 40 | }); 41 | 42 | const appModal = useDisclosure(); 43 | 44 | return ( 45 | 53 | } 56 | onClick={() => appModal.onOpen()} 57 | /> 58 | 59 | } 60 | > 61 | 62 | {team.name || team.slug} - Apps 63 | 64 | 65 | { 69 | try { 70 | const app: IApp = await fetchApi(`/teams/${team.slug}/apps`, { 71 | headers: { 72 | "Content-Type": "application/json", 73 | }, 74 | method: "POST", 75 | body: JSON.stringify({ slug: v.slug }), 76 | }); 77 | 78 | mutateApps([...apps, app], false); 79 | 80 | appModal.onClose(); 81 | 82 | router.push(`/apps/${v.slug}/deploy`); 83 | } catch (e) { 84 | if (e.resp?.status === 409) { 85 | setErrors({ 86 | slug: "This name is already taken by another app.", 87 | }); 88 | } 89 | } 90 | 91 | setSubmitting(false); 92 | }} 93 | /> 94 | 95 | {apps.length > 0 ? ( 96 | 101 | {apps.map((app: IApp) => { 102 | return ( 103 | 109 | ); 110 | })} 111 | 112 | ) : ( 113 | This team doesn't have any apps yet 😢 114 | )} 115 | 116 | ); 117 | } 118 | 119 | export const getServerSideProps: GetServerSideProps = withCookies( 120 | async (ctx) => { 121 | try { 122 | const [user, team, users, apps] = await Promise.all( 123 | [ 124 | "/users/me", 125 | `/teams/${ctx.params.id}`, 126 | `/teams/${ctx.params.id}/users`, 127 | `/teams/${ctx.params.id}/apps`, 128 | ].map((i) => fetchSSR(i, ctx)) 129 | ); 130 | 131 | return { 132 | props: { 133 | user, 134 | users, 135 | team, 136 | apps, 137 | }, 138 | }; 139 | } catch (e) { 140 | if (e.url == "/users/me") { 141 | return { 142 | redirect: { 143 | destination: "/api/login", 144 | permanent: false, 145 | }, 146 | }; 147 | } else { 148 | return { 149 | notFound: true, 150 | }; 151 | } 152 | } 153 | } 154 | ); 155 | -------------------------------------------------------------------------------- /src/pages/teams/[id]/settings.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import useSWR from "swr"; 3 | import { fetchSSR } from "../../../lib/fetch"; 4 | import { 5 | Heading, 6 | Button, 7 | Text, 8 | Box, 9 | Grid, 10 | Input, 11 | useColorMode, 12 | FormControl, 13 | FormErrorMessage, 14 | useDisclosure, 15 | } from "@chakra-ui/react"; 16 | import { ConfirmDelete } from "../../../components/ConfirmDelete"; 17 | 18 | import { GetServerSideProps } from "next"; 19 | import { IApp, ITeam, IUser } from "../../../types/haas"; 20 | import TeamLayout from "../../../layouts/TeamLayout"; 21 | import React, { useState } from "react"; 22 | import Head from "next/head"; 23 | import { withCookies } from "../../../components/Chakra"; 24 | 25 | export default function TeamSettingsPage(props: { 26 | user: IUser; 27 | users: IUser[]; 28 | team: ITeam; 29 | apps: IApp[]; 30 | }) { 31 | const router = useRouter(); 32 | const { id } = router.query; 33 | 34 | const { data: team } = useSWR(`/teams/${id}`, { 35 | fallbackData: props.team, 36 | }); 37 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 38 | const { data: users } = useSWR(`/teams/${id}/users`, { 39 | fallbackData: props.users, 40 | }); 41 | const { data: apps } = useSWR(`/teams/${id}/apps`, { 42 | fallbackData: props.apps, 43 | }); 44 | const { 45 | isOpen: manageIsOpen, 46 | onOpen: manageOnOpen, 47 | onClose: manageOnClose, 48 | } = useDisclosure(); 49 | const { 50 | isOpen: enableIsOpen, 51 | onOpen: enableOnOpen, 52 | onClose: enableOnClose, 53 | } = useDisclosure(); 54 | const { 55 | isOpen: confirmIsOpen, 56 | onOpen: confirmOnOpen, 57 | onClose: confirmOnClose, 58 | } = useDisclosure(); 59 | 60 | const [verb, setVerb] = useState("delete"); 61 | 62 | const { colorMode } = useColorMode(); 63 | 64 | return ( 65 | 72 | 73 | {team.name || team.slug} - Settings 74 | 75 | 76 | 77 | 78 | Settings 79 | 80 | 81 | Name 82 | 83 | 84 | 85 | 93 | 94 | This field is required. 95 | 96 | 97 | 100 | 101 | 102 | Delete 103 | 104 | Warning: This will: 105 | 106 |
    107 |
  • discard your HN balance
  • 108 |
  • 109 | destroy all data from this team's apps, controls, and 110 | add-ons 111 |
  • 112 |
  • stop any running containers
  • 113 |
  • release any domains associated with this team's apps
  • 114 |
115 |
116 | 117 | You will be asked to confirm this action in order to proceed. 118 | 119 | 129 | {}} 137 | /> 138 |
139 |
140 |
141 | ); 142 | } 143 | 144 | export const getServerSideProps: GetServerSideProps = withCookies( 145 | async (ctx) => { 146 | try { 147 | const [user, team, users, apps] = await Promise.all( 148 | [ 149 | "/users/me", 150 | `/teams/${ctx.params.id}`, 151 | `/teams/${ctx.params.id}/users`, 152 | `/teams/${ctx.params.id}/apps`, 153 | ].map((i) => fetchSSR(i, ctx)) 154 | ); 155 | 156 | return { 157 | props: { 158 | user, 159 | users, 160 | team, 161 | apps, 162 | }, 163 | }; 164 | } catch (e) { 165 | if (e.url == "/users/me") { 166 | return { 167 | redirect: { 168 | destination: "/api/login", 169 | permanent: false, 170 | }, 171 | }; 172 | } else { 173 | return { 174 | notFound: true, 175 | }; 176 | } 177 | } 178 | } 179 | ); 180 | -------------------------------------------------------------------------------- /src/pages/teams/[id]/users.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import useSWR from "swr"; 3 | import { fetchSSR } from "../../../lib/fetch"; 4 | 5 | import { GetServerSideProps } from "next"; 6 | import { IApp, ITeam, IUser } from "../../../types/haas"; 7 | import TeamLayout from "../../../layouts/TeamLayout"; 8 | import React from "react"; 9 | 10 | import { 11 | Avatar, 12 | Flex, 13 | Table, 14 | Tbody, 15 | Td, 16 | Text, 17 | Th, 18 | Thead, 19 | Tr, 20 | Badge, 21 | Heading, 22 | Tooltip, 23 | useClipboard, 24 | Button, 25 | } from "@chakra-ui/react"; 26 | import Head from "next/head"; 27 | import { withCookies } from "../../../components/Chakra"; 28 | import Icon from "@hackclub/icons"; 29 | 30 | export default function TeamPage(props: { 31 | user: IUser; 32 | users: IUser[]; 33 | team: ITeam; 34 | apps: IApp[]; 35 | }) { 36 | const router = useRouter(); 37 | const { id } = router.query; 38 | 39 | const { data: team } = useSWR(`/teams/${id}`, { 40 | fallbackData: props.team, 41 | }); 42 | const { data: user } = useSWR("/users/me", { fallbackData: props.user }); 43 | const { data: users } = useSWR(`/teams/${id}/users`, { 44 | fallbackData: props.users, 45 | }); 46 | const { data: apps } = useSWR(`/teams/${id}/apps`, { 47 | fallbackData: props.apps, 48 | }); 49 | 50 | const { hasCopied, onCopy } = useClipboard(team.invite); 51 | 52 | return ( 53 | 61 | 75 | 76 | } 77 | > 78 | 79 | {team.name || team.slug} - Users 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {users.map((u) => ( 91 | 92 | 103 | 104 | ))} 105 | 106 |
Name
93 | 94 | 95 | 96 | 97 | {u.name} 98 | {" "} 99 | {u.id == user.id && You} 100 | 101 | 102 |
107 |
108 | ); 109 | } 110 | 111 | export const getServerSideProps: GetServerSideProps = withCookies( 112 | async (ctx) => { 113 | try { 114 | const [user, team, users, apps] = await Promise.all( 115 | [ 116 | "/users/me", 117 | `/teams/${ctx.params.id}`, 118 | `/teams/${ctx.params.id}/users`, 119 | `/teams/${ctx.params.id}/apps`, 120 | ].map((i) => fetchSSR(i, ctx)) 121 | ); 122 | 123 | return { 124 | props: { 125 | user, 126 | users, 127 | team, 128 | apps, 129 | }, 130 | }; 131 | } catch (e) { 132 | if (e.url == "/users/me") { 133 | return { 134 | redirect: { 135 | destination: "/api/login", 136 | permanent: false, 137 | }, 138 | }; 139 | } else { 140 | return { 141 | notFound: true, 142 | }; 143 | } 144 | } 145 | } 146 | ); 147 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap"); 2 | -------------------------------------------------------------------------------- /src/styles/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Slightly modified version of https://github.com/rstacruz/nprogress/blob/master/nprogress.css */ 2 | 3 | /* Make clicks pass-through */ 4 | #nprogress { 5 | pointer-events: none; 6 | } 7 | 8 | #nprogress .bar { 9 | background: #ec3750; 10 | 11 | position: fixed; 12 | z-index: 1031; 13 | top: 0; 14 | left: 0; 15 | 16 | width: 100%; 17 | height: 2px; 18 | } 19 | 20 | /* Fancy blur effect */ 21 | #nprogress .peg { 22 | display: block; 23 | position: absolute; 24 | right: 0px; 25 | width: 100px; 26 | height: 100%; 27 | box-shadow: 0 0 10px #ff8c37, 0 0 5px #ff8c37; 28 | opacity: 1; 29 | 30 | -webkit-transform: rotate(3deg) translate(0px, -4px); 31 | -ms-transform: rotate(3deg) translate(0px, -4px); 32 | transform: rotate(3deg) translate(0px, -4px); 33 | } 34 | 35 | /* Remove these to get rid of the spinner */ 36 | #nprogress .spinner { 37 | display: block; 38 | position: fixed; 39 | z-index: 1031; 40 | top: 15px; 41 | right: 15px; 42 | } 43 | 44 | #nprogress .spinner-icon { 45 | width: 18px; 46 | height: 18px; 47 | box-sizing: border-box; 48 | 49 | border: solid 2px transparent; 50 | border-top-color: #29d; 51 | border-left-color: #29d; 52 | border-radius: 50%; 53 | 54 | -webkit-animation: nprogress-spinner 400ms linear infinite; 55 | animation: nprogress-spinner 400ms linear infinite; 56 | } 57 | 58 | .nprogress-custom-parent { 59 | overflow: hidden; 60 | position: relative; 61 | } 62 | 63 | .nprogress-custom-parent #nprogress .spinner, 64 | .nprogress-custom-parent #nprogress .bar { 65 | position: absolute; 66 | } 67 | 68 | @-webkit-keyframes nprogress-spinner { 69 | 0% { 70 | -webkit-transform: rotate(0deg); 71 | } 72 | 100% { 73 | -webkit-transform: rotate(360deg); 74 | } 75 | } 76 | @keyframes nprogress-spinner { 77 | 0% { 78 | transform: rotate(0deg); 79 | } 80 | 100% { 81 | transform: rotate(360deg); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from "@chakra-ui/react"; 2 | 3 | export default extendTheme({ 4 | fonts: { 5 | heading: `"Phantom Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`, 6 | mono: `"JetBrains Mono", monospace`, 7 | }, 8 | initialColorMode: "dark", 9 | useSystemColorMode: true, 10 | colors: { 11 | darker: "#121217", 12 | dark: "#17171d", 13 | darkless: "#252429", 14 | black: "#1f2d3d", 15 | steel: "#273444", 16 | slate: "#3c4858", 17 | muted: "#8492a6", 18 | smoke: "#e0e6ed", 19 | snow: "#f9fafc", 20 | elevated: "#ffffff", 21 | sheet: "#f9fafc", 22 | sunken: "#e0e6ed", 23 | }, 24 | components: { 25 | // Input: { 26 | // parts: ["field"], 27 | // baseStyle: { 28 | // field: { 29 | // border: "2px solid grey", 30 | // }, 31 | // }, 32 | // }, 33 | Button: { 34 | baseStyle: { 35 | fontFamily: "heading", 36 | }, 37 | variants: { 38 | cta: { 39 | bg: "linear-gradient(-45deg, #ec3750, #ff8c37)", 40 | color: "white", 41 | _hover: { 42 | _disabled: { 43 | bg: "linear-gradient(-45deg, #ec3750, #ff8c37)", 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | Avatar: { 50 | parts: ["container"], 51 | baseStyle: { 52 | container: { 53 | boxShadow: "0 4px 12px 0 rgba(0,0,0,.1)", 54 | }, 55 | }, 56 | }, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /src/types/build.ts: -------------------------------------------------------------------------------- 1 | // see https://github.com/hack-as-a-service/api/blob/06c43ad2284e12a0877e2af564df22b866895804/crates/provisioner/src/lib.rs, lines 30-62 for context 2 | 3 | export type IDockerBuildEvent = { 4 | id?: string; 5 | stream?: string; 6 | error?: string; 7 | error_detail?: { 8 | code: number; 9 | message?: string; 10 | }; 11 | status?: string; 12 | progress?: string; 13 | progressDetail?: { 14 | current: number; 15 | total: number; 16 | }; 17 | aux?: { 18 | id?: string; 19 | }; 20 | }; 21 | 22 | export type TGitCloneEvent = string; 23 | 24 | export type TProvisionerDeployEvent = string; 25 | 26 | export type IPayload = { 27 | ts: string; 28 | } & ({ Ok: Event } | { Err: string }); 29 | 30 | type Event = 31 | | { type: "docker_build"; event: IDockerBuildEvent } 32 | | { type: "git_clone"; event: TGitCloneEvent } 33 | | { type: "deploy"; event: TProvisionerDeployEvent }; 34 | 35 | // ProvisionerDeployEvent interfaces 36 | 37 | interface IDeployBeginEvent { 38 | deploy_begin: { 39 | app_id: number; 40 | image_id: string; 41 | }; 42 | } 43 | 44 | interface ICreatingNetworkEvent { 45 | creating_network: { network_name: string }; 46 | } 47 | 48 | interface ICreatedNetworkEvent { 49 | created_network: { network_id: string }; 50 | } 51 | 52 | interface IUsingExistingNetworkEvent { 53 | using_existing_network: { network_id: string }; 54 | } 55 | 56 | interface ICreatingNewContainerEvent { 57 | creating_new_container: unknown; 58 | } 59 | 60 | interface ICreatedNewContainerEvent { 61 | created_new_container: { 62 | container_id: string; 63 | }; 64 | } 65 | 66 | interface IStartingNewContainerEvent { 67 | starting_new_container: unknown; 68 | } 69 | 70 | interface IStartedNewContainerEvent { 71 | started_new_container: unknown; 72 | } 73 | 74 | interface IRetrievingContainerIPEvent { 75 | retrieving_container_i_p: unknown; 76 | } 77 | 78 | interface IRetrievedContainerIPEvent { 79 | retrieved_container_i_p: { 80 | container_ip: string; 81 | }; 82 | } 83 | 84 | interface IAddingNewContainerAsUpstreamEvent { 85 | adding_new_container_as_upstream: unknown; 86 | } 87 | 88 | interface ICreatingNewRouteEvent { 89 | creating_new_route: { 90 | route_id: string; 91 | }; 92 | } 93 | 94 | interface IRemovingOldContainerAsUpstreamEvent { 95 | removing_old_container_as_upstream: unknown; 96 | } 97 | 98 | interface IStoppingOldContainerEvent { 99 | stopping_old_container: { 100 | container_id: string; 101 | }; 102 | } 103 | 104 | interface IDeletingOldContainerEvent { 105 | deleting_old_container: unknown; 106 | } 107 | 108 | interface IDeployEndEvent { 109 | deploy_end: { 110 | app_id: number; 111 | app_slug: string; 112 | }; 113 | } 114 | 115 | interface IOtherEvent { 116 | other: { 117 | log: string; 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/types/glyph.ts: -------------------------------------------------------------------------------- 1 | // Glyph type for Hack Club icons 2 | export type Glyph = 3 | | "analytics" 4 | | "announcement" 5 | | "attachment" 6 | | "bug" 7 | | "bug-fill" 8 | | "channel" 9 | | "channel-private" 10 | | "checkbox" 11 | | "checkmark" 12 | | "code" 13 | | "copy" 14 | | "copy-check" 15 | | "community" 16 | | "controls" 17 | | "delete" 18 | | "door-enter" 19 | | "door-leave" 20 | | "down" 21 | | "down-caret" 22 | | "down-fill" 23 | | "edit" 24 | | "email" 25 | | "email-fill" 26 | | "embed" 27 | | "emoji" 28 | | "enter" 29 | | "everything" 30 | | "expand" 31 | | "explore" 32 | | "facebook" 33 | | "flag" 34 | | "flag-fill" 35 | | "freeze" 36 | | "friend" 37 | | "github" 38 | | "google" 39 | | "home" 40 | | "idea" 41 | | "inserter" 42 | | "like" 43 | | "like-fill" 44 | | "link" 45 | | "markdown" 46 | | "member-add" 47 | | "member-remove" 48 | | "mention" 49 | | "menu" 50 | | "message" 51 | | "message-simple" 52 | | "message-fill" 53 | | "message-new" 54 | | "message-simple-new" 55 | | "minus" 56 | | "minus-fill" 57 | | "mute" 58 | | "notification" 59 | | "notification-fill" 60 | | "person" 61 | | "photo" 62 | | "photo-fill" 63 | | "pin" 64 | | "pin-fill" 65 | | "plus" 66 | | "plus-fill" 67 | | "post" 68 | | "post-cancel" 69 | | "post-fill" 70 | | "private" 71 | | "private-outline" 72 | | "private-unlocked" 73 | | "private-fill" 74 | | "profile" 75 | | "profile-fill" 76 | | "quote" 77 | | "rep" 78 | | "reply" 79 | | "sam" 80 | | "search" 81 | | "send" 82 | | "send-fill" 83 | | "settings" 84 | | "share" 85 | | "support" 86 | | "support-fill" 87 | | "thread" 88 | | "thumbsdown" 89 | | "thumbsdown-fill" 90 | | "thumbsup" 91 | | "thumbsup-fill" 92 | | "twitter" 93 | | "up" 94 | | "up-fill" 95 | | "view" 96 | | "view-fill" 97 | | "view-back" 98 | | "view-close" 99 | | "view-close-small" 100 | | "view-forward" 101 | | "view-reload" 102 | | "welcome" 103 | | "bag" 104 | | "bag-add" 105 | | "bank-account" 106 | | "bank-circle" 107 | | "card" 108 | | "card-add" 109 | | "clock" 110 | | "clock-fill" 111 | | "docs" 112 | | "docs-fill" 113 | | "event-add" 114 | | "event-cancel" 115 | | "event-code" 116 | | "external" 117 | | "external-fill" 118 | | "figma" 119 | | "figma-fill" 120 | | "filter" 121 | | "history" 122 | | "important" 123 | | "important-fill" 124 | | "medium" 125 | | "medium-fill" 126 | | "instagram" 127 | | "message-simple-fill" 128 | | "payment" 129 | | "payment-docs" 130 | | "payment-transfer" 131 | | "purse" 132 | | "purse-fill" 133 | | "shirt" 134 | | "shirt-fill" 135 | | "up-caret" 136 | | "youtube" 137 | | "zoom-in" 138 | | "zoom-out" 139 | | "admin-badge" 140 | | "admin" 141 | | "align-center" 142 | | "align-left" 143 | | "align-right" 144 | | "battery-bolt" 145 | | "battery-fill" 146 | | "battery" 147 | | "bolt-circle" 148 | | "bolt-docs" 149 | | "bolt" 150 | | "briefcase" 151 | | "clubs-fill" 152 | | "clubs" 153 | | "compass" 154 | | "crosshairs" 155 | | "download" 156 | | "event-check" 157 | | "food" 158 | | "forbidden" 159 | | "grid" 160 | | "help" 161 | | "info" 162 | | "leader" 163 | | "leaders" 164 | | "list" 165 | | "meh" 166 | | "messenger-fill" 167 | | "messenger" 168 | | "more-fill" 169 | | "more" 170 | | "relaxed" 171 | | "rss" 172 | | "sad" 173 | | "slack-fill" 174 | | "channels" 175 | | "slack" 176 | | "sticker" 177 | | "terminal" 178 | | "transactions" 179 | | "twitch" 180 | | "web" 181 | | "wifi" 182 | | "apple" 183 | | "windows"; 184 | -------------------------------------------------------------------------------- /src/types/haas.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: string; 3 | name: string; 4 | avatar?: string; 5 | slack_user_id: string; 6 | } 7 | 8 | export interface ITeam { 9 | id: number; 10 | name?: string; 11 | avatar?: string; 12 | slug: string; 13 | personal: boolean; 14 | invite: string; 15 | } 16 | 17 | export interface IApp { 18 | id: number; 19 | team_id: number; 20 | slug: string; 21 | enabled: boolean; 22 | created_at: string; 23 | } 24 | 25 | export interface IDomain { 26 | id: number; 27 | domain: string; 28 | verified: boolean; 29 | app_id: number; 30 | } 31 | 32 | export interface IBuild { 33 | id: number; 34 | app_id: number; 35 | started_at: string; 36 | ended_at?: string; 37 | events: string[]; 38 | } 39 | 40 | export type KVConfig = { 41 | [id: string]: { 42 | key: string; 43 | keyEditable: boolean; 44 | valueEditable: boolean; 45 | obscureValue: boolean; 46 | value: string; 47 | }; 48 | }; 49 | 50 | export interface IAddon { 51 | name: string; 52 | activated: boolean; 53 | description: string; 54 | img: string; 55 | id: string; 56 | config: KVConfig; 57 | storage: string; 58 | price: string; 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------