diff --git a/budget-app/.env.example b/budget-app/.env.example index 5a1ea58..335b6b2 100644 --- a/budget-app/.env.example +++ b/budget-app/.env.example @@ -10,4 +10,6 @@ AZURE_STORAGE_CONNECTION_STRING=UseDevelopmentStorage=true AZURE_STORAGE_QUEUE_NAME=budgetqueue AZURE_FUNCTION_RESET_CACHE_PATH=/api/resetcache -NEXT_PUBLIC_AZURE_FUNCTION_URL=http://localhost:7072 \ No newline at end of file +NEXT_PUBLIC_AZURE_FUNCTION_URL=http://localhost:7072 + +GITHUB_TOKEN= \ No newline at end of file diff --git a/budget-app/package-lock.json b/budget-app/package-lock.json index f2dee44..de60184 100644 --- a/budget-app/package-lock.json +++ b/budget-app/package-lock.json @@ -20,6 +20,7 @@ "@mui/material": "^5.15.15", "@mui/material-nextjs": "^5.15.11", "@mui/x-date-pickers": "^7.1.1", + "@octokit/core": "^6.1.2", "@tanstack/query-sync-storage-persister": "^5.29.0", "@tanstack/react-query": "^5.29.2", "@tanstack/react-query-persist-client": "^5.29.2", @@ -3421,6 +3422,94 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "dependencies": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "dependencies": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "dependencies": { + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "node_modules/@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", + "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "dependencies": { + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", @@ -5226,6 +5315,11 @@ } ] }, + "node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -10882,6 +10976,11 @@ "node": ">=8" } }, + "node_modules/universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -14092,6 +14191,76 @@ "fastq": "^1.6.0" } }, + "@octokit/auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==" + }, + "@octokit/core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", + "requires": { + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + } + }, + "@octokit/endpoint": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", + "requires": { + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" + } + }, + "@octokit/graphql": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", + "requires": { + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + } + }, + "@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + }, + "@octokit/request": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", + "requires": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" + } + }, + "@octokit/request-error": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", + "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", + "requires": { + "@octokit/types": "^13.0.0" + } + }, + "@octokit/types": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", + "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "requires": { + "@octokit/openapi-types": "^22.2.0" + } + }, "@opentelemetry/api": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", @@ -15342,6 +15511,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==" + }, "bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -19333,6 +19507,11 @@ "crypto-random-string": "^2.0.0" } }, + "universal-user-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" + }, "universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/budget-app/package.json b/budget-app/package.json index 2017968..661e46e 100644 --- a/budget-app/package.json +++ b/budget-app/package.json @@ -23,6 +23,7 @@ "@mui/material": "^5.15.15", "@mui/material-nextjs": "^5.15.11", "@mui/x-date-pickers": "^7.1.1", + "@octokit/core": "^6.1.2", "@tanstack/query-sync-storage-persister": "^5.29.0", "@tanstack/react-query": "^5.29.2", "@tanstack/react-query-persist-client": "^5.29.2", diff --git a/budget-app/src/app/api/scale/route.ts b/budget-app/src/app/api/scale/route.ts new file mode 100644 index 0000000..f314595 --- /dev/null +++ b/budget-app/src/app/api/scale/route.ts @@ -0,0 +1,31 @@ +import { octokit } from "@/bootstrap"; +import { env } from "@/env"; +import { scaleContainerApp } from "@/libs/github"; +import { NextResponse } from "next/server"; + +/** + * Scale Up Azure Container App (Set min_replicas to 1) + * @param req + * @returns + */ +export async function GET(req: Request) { + // TODO: Hack for Next.js Build, prevent static optimization + if (env.GITHUB_TOKEN === "") { + return NextResponse.json({ + message: "GITHUB_TOKEN is not set in .env", + }, { + status: 500, + }); + } + await scaleContainerApp(octokit, { + owner: "mildronize", + repo: "bunsheet", + ref: "main", + inputs: { + min_replicas: 1, + }, + }) + return NextResponse.json({ + message: "Sent request to scale up Azure Container App, check the action logs in the repository.", + }); +} diff --git a/budget-app/src/app/api/transaction/route.ts b/budget-app/src/app/api/transaction/route.ts index f7012d5..28cc26b 100644 --- a/budget-app/src/app/api/transaction/route.ts +++ b/budget-app/src/app/api/transaction/route.ts @@ -3,9 +3,6 @@ import { globalHandler } from "@/global/globalHandler"; import { queue, sheetDoc, transactionCacheTable } from "@/bootstrap"; import { NextResponse } from "next/server"; import { z } from "zod"; -import { updateExistingSheet } from "@/libs/google-sheet"; -import { env } from "@/env"; -import { ODataExpression } from "ts-odata-client"; import { TransactionCacheEntity } from "@/entites/transaction.entity"; import dayjs from "dayjs"; // https://github.com/vercel/next.js/issues/58242 diff --git a/budget-app/src/app/tabs/SettingTab.tsx b/budget-app/src/app/tabs/SettingTab.tsx index 55f6d95..cda63a1 100644 --- a/budget-app/src/app/tabs/SettingTab.tsx +++ b/budget-app/src/app/tabs/SettingTab.tsx @@ -1,6 +1,6 @@ "use client"; import { InferRouteResponse } from "@/types"; -import * as Transaction from "@/app/api/transaction/route"; +import * as ScaleApi from "@/app/api/scale/route"; import { Box, Button, Typography } from "@mui/material"; import { queryClient } from "../components/ReactQueryClientProvider"; import CleaningServicesRoundedIcon from "@mui/icons-material/CleaningServicesRounded"; @@ -10,13 +10,12 @@ import axios from "axios"; import { LocalStorage } from "@/libs/local-storage"; import { useState } from "react"; import { useGlobalLoading } from "@/hooks/useGlobalLoading"; -import { useSignalR } from "@/hooks/useSignalR"; import { useVersion } from "@/hooks/useVersion"; -export type TransactionGetResponse = InferRouteResponse; +export type ScaleGetResponse = InferRouteResponse; export function SettingTab() { - const [isResettingCache, setIsResettingCache] = useState(false); + const [isLoading, setIsLoading] = useState(false); const clearAppCache = () => { const caches = [ @@ -28,7 +27,7 @@ export function SettingTab() { const resetCache = async () => { try { - setIsResettingCache(true); + setIsLoading(true); clearAppCache(); await axios.get("/api/cache/reset"); /** @@ -41,10 +40,23 @@ export function SettingTab() { toast.error("Failed to reset cache"); return; } - setIsResettingCache(false); + setIsLoading(false); }; - useGlobalLoading(isResettingCache); + const scaleUp = async () => { + setIsLoading(true); + try { + const result = await axios.get("/api/scale"); + const { message } = result.data; + toast.success(message); + } catch (error) { + toast.error("Failed to scale up Azure Container App"); + return; + } + setIsLoading(false); + } + + useGlobalLoading(isLoading); const version = useVersion(); const reloadPage = () => { @@ -54,11 +66,22 @@ export function SettingTab() { return ( -
+

Scale Settings

+
+
+

Cache Settings

+
+ + diff --git a/budget-app/src/bootstrap.ts b/budget-app/src/bootstrap.ts index 95b67bb..316c55d 100644 --- a/budget-app/src/bootstrap.ts +++ b/budget-app/src/bootstrap.ts @@ -95,3 +95,11 @@ export const sheetDoc = new GoogleSpreadsheet( env.GSHEET_SPREADSHEET_ID, serviceAccountAuth ); + +/** + * Github Service + */ +import { Octokit } from '@octokit/core'; +export const octokit = new Octokit({ + auth: env.GITHUB_TOKEN, +}); diff --git a/budget-app/src/env.ts b/budget-app/src/env.ts index 5abdb7a..44c2f80 100644 --- a/budget-app/src/env.ts +++ b/budget-app/src/env.ts @@ -121,6 +121,10 @@ export const envSchema = z.object({ * Azure Function URL */ NEXT_PUBLIC_AZURE_FUNCTION_URL: z.string().default("http://localhost:7071"), + /** + * Github Token for Scaling Container App + */ + GITHUB_TOKEN: z.string().default(""), }); function printSecretFields( diff --git a/budget-app/src/libs/github.ts b/budget-app/src/libs/github.ts new file mode 100644 index 0000000..48a2c57 --- /dev/null +++ b/budget-app/src/libs/github.ts @@ -0,0 +1,27 @@ +import { Octokit } from '@octokit/core'; + +const headers = { + 'X-GitHub-Api-Version': '2022-11-28', +}; + +interface ScaleContainerAppConfig { + owner: string; + repo: string; + ref: string; + inputs: { + min_replicas: number; + }; +} + +export async function scaleContainerApp(octokit: Octokit, config: ScaleContainerAppConfig) { + return octokit.request('POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches', { + owner: config.owner, + repo: config.repo, + workflow_id: 'scale-azure-container-app.yml', + ref: config.ref, + inputs: { + min_replicas: String(config.inputs.min_replicas), + }, + headers, + }); +} \ No newline at end of file diff --git a/budget-app/src/version.json b/budget-app/src/version.json index 7a32b32..0143535 100644 --- a/budget-app/src/version.json +++ b/budget-app/src/version.json @@ -1,7 +1,7 @@ { "major": 1, - "minor": 5, + "minor": 6, "patch": 0, - "tag": "latest", + "tag": "canary", "revision": 0 }