├── .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 |
96 | {activated ? "Manage" : "Enable"}
97 |
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 | {
148 | setVerb("disable");
149 | manageOnClose();
150 | confirmOnOpen();
151 | }}
152 | >
153 | Disable Addon
154 |
155 | {
162 | setVerb("wipe");
163 | manageOnClose();
164 | confirmOnOpen();
165 | }}
166 | >
167 | Wipe Data Only
168 |
169 | {
176 | devAddons[id]["config"] = newConfig;
177 | manageOnClose();
178 | }}
179 | >
180 | Save
181 |
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 | {
213 | // const obj = {
214 | // name,
215 | // activated,
216 | // id,
217 | // config: c,
218 | // img,
219 | // storage,
220 | // description,
221 | // };
222 | const idx = devAddons.findIndex((o) => o.id == id);
223 | devAddons[idx].activated = true;
224 | updateActive(true);
225 | enableOnClose();
226 | }}
227 | >
228 | Confirm
229 |
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 |
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 | {
77 | onClose();
78 | onCancellation();
79 | }}
80 | >
81 | Cancel
82 |
83 | {
86 | setValue("");
87 | onClose();
88 | onConfirmation();
89 | }}
90 | colorScheme="red"
91 | >
92 | {buttonText ??
93 | (verb && verb.toUpperCase().substr(0, 1) + verb.substr(1)) ??
94 | "Delete"}
95 |
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 | Type
42 | Name
43 | Value
44 |
45 |
46 |
47 |
48 | {split.length == 2 ? "A" : "CNAME"}
49 |
50 | {split.length == 2 ? "@" : split.slice(0, -2).join(".")}
51 |
52 |
53 | {split.length == 2 ? "167.99.113.134" : "hackclub.app."}{" "}
54 |
55 | {hasCopied ? "Copied" : "Copy"}
56 |
57 |
58 |
59 |
60 |
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 |
142 | Activate
143 |
144 | )}
145 | Delete
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 | toggleVal(!hide)}
84 | >
85 | {!hide ? "Hide" : "Show"}
86 |
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 |
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 |
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 | }>Go home
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 |
51 | Deploy
52 |
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 |
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 |
82 | setEnv((e) =>
83 | e.concat({ key: "", value: "", id: Math.random().toString() })
84 | )
85 | }
86 | >
87 | Add Pair
88 |
89 |
90 | Save
91 |
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 |
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 |
23 | Login
24 |
25 |
26 |
27 |
36 |
37 | A managed platform for makers.
38 |
39 |
40 |
41 | Start Building
42 |
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 |
60 | Delete
61 |
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 | Export
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 |
98 | Update
99 |
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 | {
122 | setVerb("delete");
123 | manageOnClose();
124 | confirmOnOpen();
125 | }}
126 | >
127 | Delete Team
128 |
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 | {
69 | onCopy();
70 | }}
71 | >
72 | {team.invite}
73 |
74 |
75 |
76 | }
77 | >
78 |
79 | {team.name || team.slug} - Users
80 |
81 |
82 |
83 |
84 |
85 | Name
86 |
87 |
88 |
89 |
90 | {users.map((u) => (
91 |
92 |
93 |
94 |
95 |
96 |
97 | {u.name}
98 | {" "}
99 | {u.id == user.id && You }
100 |
101 |
102 |
103 |
104 | ))}
105 |
106 |
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 |
--------------------------------------------------------------------------------