- コールバック成功時のリダイレクト先をWEB_ORIGINルートから /dashboardに変更(ランディングで止まらないように) - ルートページでログイン済みならダッシュボードへ自動遷移 - サイドバーのユーザーカードをOIDC identityの表示名・メールに変更し、 未ログイン時のみSSOログインリンクを表示 - demoヘッダ(x-demo-user)はdemoモード時のみ送信
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import type { AppRecord, ReleaseRecord, SourceRecord } from "@/packages/core/src/index";
|
|
|
|
export type DashboardSource = {
|
|
source: SourceRecord;
|
|
apps: AppRecord[];
|
|
releases: ReleaseRecord[];
|
|
};
|
|
|
|
export type DashboardData = {
|
|
workspace: {
|
|
name: string;
|
|
slug: string;
|
|
};
|
|
sources: DashboardSource[];
|
|
usage: {
|
|
storageBytes: number;
|
|
sourceCount: number;
|
|
};
|
|
limits: {
|
|
maxSources: number;
|
|
maxAppsPerSource: number;
|
|
maxStorageBytes: number;
|
|
maxUploadBytes: number;
|
|
};
|
|
};
|
|
|
|
export type Notice = {
|
|
tone: "success" | "error" | "info";
|
|
text: string;
|
|
};
|
|
|
|
export type Visibility = SourceRecord["visibility"];
|
|
|
|
// Single-user deployment: hard-code the public API origin. The API is served
|
|
// at api.altdock.app.amania.jp; there is no per-environment fallback anymore.
|
|
export const API_BASE = "https://api.altdock.app.amania.jp";
|
|
export const AUTH_MODE = process.env.NEXT_PUBLIC_AUTH_MODE || "demo";
|
|
export const DEMO_HEADERS = { "x-demo-user": "demo@altdock.local" };
|
|
|
|
export async function api<T>(path: string, init: RequestInit = {}) {
|
|
const headers = new Headers(init.headers);
|
|
headers.set("accept", "application/json");
|
|
if (init.body) headers.set("content-type", "application/json");
|
|
if (AUTH_MODE === "demo") {
|
|
Object.entries(DEMO_HEADERS).forEach(([key, value]) => headers.set(key, value));
|
|
}
|
|
const response = await fetch(`${API_BASE}${path}`, { ...init, headers, credentials: "include" });
|
|
const body = (await response.json().catch(() => ({}))) as T & { error?: string; detail?: string };
|
|
if (!response.ok) throw new Error(body.detail || body.error || `Request failed (${response.status})`);
|
|
return body;
|
|
}
|
|
|
|
export function formatBytes(value: number) {
|
|
if (!value) return "0 B";
|
|
const units = ["B", "KB", "MB", "GB"];
|
|
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
|
|
return `${(value / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
|
|
}
|
|
|
|
export function formatDate(value: string) {
|
|
return new Intl.DateTimeFormat("ja-JP", { year: "numeric", month: "short", day: "numeric" }).format(new Date(value));
|
|
}
|
|
|
|
export function formatDateTime(value: string) {
|
|
return new Intl.DateTimeFormat("ja-JP", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value));
|
|
}
|
|
|
|
export function statusLabel(status: ReleaseRecord["status"]) {
|
|
return {
|
|
uploaded: "アップロード済み",
|
|
processing: "処理中",
|
|
ready: "公開準備完了",
|
|
rejected: "要修正",
|
|
published: "公開中",
|
|
archived: "アーカイブ",
|
|
}[status];
|
|
}
|
|
|
|
export function statusTone(status: ReleaseRecord["status"]) {
|
|
return {
|
|
uploaded: "neutral",
|
|
processing: "info",
|
|
ready: "warning",
|
|
rejected: "danger",
|
|
published: "success",
|
|
archived: "neutral",
|
|
}[status] as "neutral" | "info" | "warning" | "danger" | "success";
|
|
}
|
|
|
|
export function visibilityLabel(visibility: Visibility) {
|
|
return { draft: "下書き", unlisted: "限定公開", public: "公開" }[visibility];
|
|
}
|
|
|
|
export function visibilityTone(visibility: Visibility) {
|
|
return { draft: "neutral", unlisted: "warning", public: "success" }[visibility] as "neutral" | "warning" | "success";
|
|
}
|
|
|
|
export function flattenSources(dashboard: DashboardData) {
|
|
return dashboard.sources;
|
|
}
|
|
|
|
export function flattenApps(dashboard: DashboardData) {
|
|
return dashboard.sources.flatMap((entry) => entry.apps.map((app) => ({ app, source: entry.source, releases: entry.releases.filter((release) => release.appId === app.id) })));
|
|
}
|
|
|
|
export function flattenReleases(dashboard: DashboardData) {
|
|
return dashboard.sources.flatMap((entry) => entry.releases.map((release) => ({ release, source: entry.source, app: entry.apps.find((app) => app.id === release.appId) })));
|
|
}
|