feat: 設定画面を実装

Workspace・認証モード・保存容量・配布上限・API情報を一覧表示する
設定画面を追加した。OIDCモードではログインユーザー表示とログアウト
ボタンを表示し、demoモードではアカウント情報を固定表示する。
- settings用のグリッド/行/バッジ/クォータスタイルをglobals.cssに追加
- AdminShellの未使用変数警告を解消
This commit is contained in:
amania-jailbreak
2026-08-05 16:27:29 +09:00
parent 55f172de3b
commit cfe5bfb788
3 changed files with 87 additions and 5 deletions
+83 -4
View File
@@ -1,7 +1,86 @@
import Link from "next/link"; "use client";
import { ArrowUpRight, KeyRound, ShieldCheck, SlidersHorizontal } from "lucide-react";
export default function SettingsPage() { import { useEffect, useState } from "react";
return <><div className="admin-page-heading"><div><div className="admin-eyebrow">WORKSPACE SETTINGS</div><h1></h1><p></p></div></div><section className="admin-section-card settings-placeholder"><div className="settings-placeholder-icon"><SlidersHorizontal size={20} /></div><h2></h2><p>SSO</p><div className="settings-placeholder-grid"><div><ShieldCheck size={17} /><strong></strong><span>OIDC / demo mode</span></div><div><KeyRound size={17} /><strong></strong><span>Workspace単位の認可</span></div></div><Link className="admin-link" href="https://faq.altstore.io/developers/distribute-with-altstore-pal" target="_blank">PALドキュメントを開く <ArrowUpRight size={14} /></Link></section></>; import { BookOpen, Database, HardDrive, KeyRound, LogOut, Server, ShieldCheck, SlidersHorizontal, User, Workflow } from "lucide-react";
import { API_BASE, AUTH_MODE, formatBytes } from "@/app/lib/dashboard";
import { useDashboard } from "@/components/altdock/dashboard-provider";
import { Button } from "@/components/ui/button";
import type { Identity } from "@/packages/core/src/index";
function SettingRow({ icon: Icon, label, value, hint }: { icon: typeof User; label: string; value: React.ReactNode; hint?: string }) {
return <div className="settings-row"><span className="settings-row-icon"><Icon size={16} strokeWidth={1.8} /></span><div className="settings-row-copy"><span>{label}</span><strong>{value}</strong>{hint && <small>{hint}</small>}</div></div>;
} }
function Section({ title, description, children }: { title: string; description: string; children: React.ReactNode }) {
return <section className="admin-section-card settings-section"><div className="admin-section-card-header"><div><h2>{title}</h2><p>{description}</p></div></div>{children}</section>;
}
export default function SettingsPage() {
const { dashboard } = useDashboard();
const [identity, setIdentity] = useState<Identity | null>(null);
useEffect(() => {
if (AUTH_MODE !== "oidc") return;
let cancelled = false;
fetch(`${API_BASE}/api/v1/me`, { credentials: "include" })
.then((response) => response.ok ? response.json() : null)
.then((body) => { if (!cancelled && body?.identity) setIdentity(body.identity as Identity); })
.catch(() => {});
return () => { cancelled = true; };
}, []);
const storagePercent = dashboard ? Math.min(100, (dashboard.usage.storageBytes / Math.max(1, dashboard.limits.maxStorageBytes)) * 100) : 0;
return <>
<div className="admin-page-heading"><div><div className="admin-eyebrow">WORKSPACE SETTINGS</div><h1></h1><p>Workspace</p></div></div>
<div className="settings-grid">
<Section title="Workspace" description="AltDock上の配布単位となるWorkspace情報です。">
<div className="settings-rows">
<SettingRow icon={Workflow} label="Workspace名" value={dashboard?.workspace.name || "—"} />
<SettingRow icon={SlidersHorizontal} label="Workspace slug" value={<code>{dashboard?.workspace.slug || "—"}</code>} />
<SettingRow icon={User} label="ログインユーザー" value={AUTH_MODE === "oidc" ? (identity?.displayName || "—") : "Demo Developer"} hint={AUTH_MODE === "oidc" ? identity?.email : "demo@altdock.local"} />
</div>
</Section>
<Section title="認証" description="このAPIに接続している認証モードです。">
<div className="settings-rows">
<SettingRow icon={ShieldCheck} label="認証モード" value={AUTH_MODE === "oidc" ? <span className="settings-badge settings-badge-ok">OIDC SSO</span> : <span className="settings-badge">Demo mode</span>} hint={AUTH_MODE === "oidc" ? "汎用OIDCプロバイダでログインしています" : "開発用の簡易認証です。本番ではOIDCを推奨します"} />
{AUTH_MODE === "oidc" && <SettingRow icon={KeyRound} label="SSOセッション" value={identity ? <span className="settings-badge settings-badge-ok"></span> : <span className="settings-badge"></span>} />}
</div>
</Section>
<Section title="保存容量" description="Workspaceで使用しているストレージの割合です。">
<div className="settings-quota">
<div className="settings-quota-top"><span>{formatBytes(dashboard?.usage.storageBytes || 0)} / {formatBytes(dashboard?.limits.maxStorageBytes || 0)}</span><strong>{storagePercent.toFixed(1)}%</strong></div>
<div className="admin-progress"><span style={{ width: `${storagePercent}%` }} /></div>
<small>ADP ZIP本体と公開済みアセットの合計サイズ</small>
</div>
</Section>
<Section title="配布上限" description="環境変数で変更できる制限値です。">
<div className="settings-rows">
<SettingRow icon={HardDrive} label="ADP ZIP 1件の上限" value={formatBytes(dashboard?.limits.maxUploadBytes || 0)} />
<SettingRow icon={Database} label="WorkspaceあたりのSource数" value={dashboard?.limits.maxSources || "—"} />
<SettingRow icon={Workflow} label="Sourceあたりのアプリ数" value={dashboard?.limits.maxAppsPerSource || "—"} />
</div>
</Section>
<Section title="API" description="フロントエンドが利用しているAPIエンドポイントです。">
<div className="settings-rows">
<SettingRow icon={Server} label="API Base URL" value={<code>{API_BASE}</code>} />
<SettingRow icon={Server} label="ヘルスチェック" value={<a className="admin-link" href={`${API_BASE}/healthz`} target="_blank" rel="noreferrer">/healthz </a>} />
<SettingRow icon={Server} label="バージョン" value={<a className="admin-link" href={`${API_BASE}/version`} target="_blank" rel="noreferrer">/version </a>} />
</div>
</Section>
</div>
<section className="admin-section-card settings-footer">
<div><ShieldCheck size={17} /><span><strong>AltDock </strong><small>Source</small></span></div>
<div className="settings-footer-actions">
<Button variant="outline" size="sm" onClick={() => window.open("https://faq.altstore.io/developers/distribute-with-altstore-pal", "_blank")}><BookOpen size={14} />PALドキュメント</Button>
{AUTH_MODE === "oidc" && <Button variant="ghost" size="sm" onClick={() => { window.location.href = `${API_BASE}/auth/logout`; }}><LogOut size={14} /></Button>}
</div>
</section>
</>;
}
+3
View File
@@ -92,6 +92,9 @@ button { cursor: pointer; }
.admin-toast-viewport { position: fixed; z-index: 100; right: 18px; bottom: 18px; display: flex; width: 360px; max-width: calc(100vw - 36px); flex-direction: column; gap: 8px; outline: 0; }.admin-toast { position: relative; display: grid; grid-template-columns: 1fr auto; gap: 3px 12px; padding: 13px 14px; border: 1px solid var(--admin-line-strong); border-left: 3px solid var(--admin-green); border-radius: 7px; color: var(--admin-text); background: #fff; box-shadow: 0 10px 32px #17202c22; }.admin-toast.toast-error { border-left-color: var(--admin-red); }.admin-toast.toast-info { border-left-color: var(--admin-blue); }.admin-toast [data-radix-toast-title] { font-size: 11px; font-weight: 720; }.admin-toast [data-radix-toast-description] { color: var(--admin-muted); font-size: 10px; line-height: 1.45; }.admin-toast button { grid-column: 2; grid-row: 1 / span 2; align-self: center; border: 0; color: var(--admin-subtle); background: transparent; font-size: 16px; } .admin-toast-viewport { position: fixed; z-index: 100; right: 18px; bottom: 18px; display: flex; width: 360px; max-width: calc(100vw - 36px); flex-direction: column; gap: 8px; outline: 0; }.admin-toast { position: relative; display: grid; grid-template-columns: 1fr auto; gap: 3px 12px; padding: 13px 14px; border: 1px solid var(--admin-line-strong); border-left: 3px solid var(--admin-green); border-radius: 7px; color: var(--admin-text); background: #fff; box-shadow: 0 10px 32px #17202c22; }.admin-toast.toast-error { border-left-color: var(--admin-red); }.admin-toast.toast-info { border-left-color: var(--admin-blue); }.admin-toast [data-radix-toast-title] { font-size: 11px; font-weight: 720; }.admin-toast [data-radix-toast-description] { color: var(--admin-muted); font-size: 10px; line-height: 1.45; }.admin-toast button { grid-column: 2; grid-row: 1 / span 2; align-self: center; border: 0; color: var(--admin-subtle); background: transparent; font-size: 16px; }
.mobile-nav-dialog { top: 0; left: 0; width: min(290px, calc(100vw - 44px)); height: 100vh; max-height: none; padding: 0; border: 0; border-radius: 0 9px 9px 0; transform: none; }.mobile-nav-dialog .admin-sidebar-inner { padding: 16px 12px; }.mobile-nav-close { position: absolute; z-index: 2; top: 16px; right: 13px; display: inline-flex; width: 28px; height: 28px; align-items: center; justify-content: center; border: 0; border-radius: 5px; color: var(--admin-subtle); background: #f1f3f5; } .mobile-nav-dialog { top: 0; left: 0; width: min(290px, calc(100vw - 44px)); height: 100vh; max-height: none; padding: 0; border: 0; border-radius: 0 9px 9px 0; transform: none; }.mobile-nav-dialog .admin-sidebar-inner { padding: 16px 12px; }.mobile-nav-close { position: absolute; z-index: 2; top: 16px; right: 13px; display: inline-flex; width: 28px; height: 28px; align-items: center; justify-content: center; border: 0; border-radius: 5px; color: var(--admin-subtle); background: #f1f3f5; }
.settings-placeholder { max-width: 620px; padding: 34px; text-align: center; }.settings-placeholder-icon { display: inline-flex; width: 42px; height: 42px; align-items: center; justify-content: center; border-radius: 8px; color: var(--admin-orange); background: #fff1ed; }.settings-placeholder h2 { margin: 14px 0 7px; color: var(--admin-text); font-size: 16px; }.settings-placeholder > p { max-width: 420px; margin: 0 auto 22px; color: var(--admin-muted); font-size: 11px; line-height: 1.6; }.settings-placeholder-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-bottom: 20px; }.settings-placeholder-grid > div { display: flex; align-items: center; gap: 8px; padding: 12px; border: 1px solid var(--admin-line); border-radius: 6px; text-align: left; }.settings-placeholder-grid svg { color: var(--admin-blue); }.settings-placeholder-grid strong, .settings-placeholder-grid span { display: block; }.settings-placeholder-grid strong { color: var(--admin-text); font-size: 10px; }.settings-placeholder-grid span { margin-top: 3px; color: var(--admin-subtle); font-size: 9px; } .settings-placeholder { max-width: 620px; padding: 34px; text-align: center; }.settings-placeholder-icon { display: inline-flex; width: 42px; height: 42px; align-items: center; justify-content: center; border-radius: 8px; color: var(--admin-orange); background: #fff1ed; }.settings-placeholder h2 { margin: 14px 0 7px; color: var(--admin-text); font-size: 16px; }.settings-placeholder > p { max-width: 420px; margin: 0 auto 22px; color: var(--admin-muted); font-size: 11px; line-height: 1.6; }.settings-placeholder-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-bottom: 20px; }.settings-placeholder-grid > div { display: flex; align-items: center; gap: 8px; padding: 12px; border: 1px solid var(--admin-line); border-radius: 6px; text-align: left; }.settings-placeholder-grid svg { color: var(--admin-blue); }.settings-placeholder-grid strong, .settings-placeholder-grid span { display: block; }.settings-placeholder-grid strong { color: var(--admin-text); font-size: 10px; }.settings-placeholder-grid span { margin-top: 3px; color: var(--admin-subtle); font-size: 9px; }
.settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px; }.settings-section { margin-bottom: 0; }.settings-section .admin-section-card-header { padding-bottom: 11px; }.settings-rows { display: flex; flex-direction: column; padding: 0 19px 17px; }.settings-row { display: flex; align-items: center; gap: 11px; min-height: 44px; padding: 9px 0; border-top: 1px solid var(--admin-line); }.settings-row:first-child { border-top: 0; }.settings-row-icon { display: inline-flex; width: 28px; height: 28px; flex: 0 0 28px; align-items: center; justify-content: center; border-radius: 6px; color: var(--admin-muted); background: #f2f4f6; }.settings-row-icon svg { color: var(--admin-muted); }.settings-row-copy { min-width: 0; flex: 1; }.settings-row-copy span { display: block; color: var(--admin-subtle); font-size: 9px; font-weight: 700; letter-spacing: .05em; }.settings-row-copy strong { display: block; overflow: hidden; margin-top: 2px; color: var(--admin-text); font-size: 11px; font-weight: 620; text-overflow: ellipsis; white-space: nowrap; }.settings-row-copy code { color: var(--admin-muted); font-family: var(--font-geist-mono), monospace; font-size: 10px; }.settings-row-copy small { display: block; margin-top: 2px; color: var(--admin-subtle); font-size: 9px; }.settings-badge { display: inline-flex; align-items: center; min-height: 19px; padding: 0 8px; border: 1px solid var(--admin-line-strong); border-radius: 99px; color: var(--admin-muted); background: #f6f7f9; font-size: 9px; font-weight: 680; }.settings-badge-ok { border-color: #cfe5da; color: var(--admin-green); background: #edf7f2; }.settings-quota { padding: 2px 19px 18px; }.settings-quota-top { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 9px; color: var(--admin-muted); font-size: 10px; }.settings-quota-top strong { color: var(--admin-text); font-family: var(--font-geist-mono), monospace; font-size: 13px; font-weight: 620; }.settings-quota small { display: block; margin-top: 8px; color: var(--admin-subtle); font-size: 9px; }.settings-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px 19px; }.settings-footer > div:first-child { display: flex; align-items: center; gap: 11px; }.settings-footer svg { color: var(--admin-orange); }.settings-footer strong, .settings-footer small { display: block; }.settings-footer strong { color: var(--admin-text); font-size: 11px; font-weight: 650; }.settings-footer small { margin-top: 2px; color: var(--admin-subtle); font-size: 9px; }.settings-footer-actions { display: flex; align-items: center; gap: 8px; }
@media (max-width: 880px) { .settings-grid { grid-template-columns: 1fr; } }
@media (max-width: 760px) { .settings-rows { padding-inline: 14px; }.settings-quota { padding-inline: 14px; }.settings-footer { align-items: start; flex-direction: column; } }
@media (max-width: 1050px) { .admin-sidebar { width: 210px; flex-basis: 210px; }.admin-sidebar.is-collapsed { width: 64px; flex-basis: 64px; }.admin-collapse-toggle { left: 192px; }.admin-sidebar.is-collapsed ~ .admin-collapse-toggle { left: 50px; }.admin-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.admin-overview-grid { grid-template-columns: 1fr; }.admin-overview-grid .admin-section-card { margin-bottom: 0; }.admin-topbar { padding-inline: 24px; }.admin-content { padding-inline: 24px; } } @media (max-width: 1050px) { .admin-sidebar { width: 210px; flex-basis: 210px; }.admin-sidebar.is-collapsed { width: 64px; flex-basis: 64px; }.admin-collapse-toggle { left: 192px; }.admin-sidebar.is-collapsed ~ .admin-collapse-toggle { left: 50px; }.admin-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }.admin-overview-grid { grid-template-columns: 1fr; }.admin-overview-grid .admin-section-card { margin-bottom: 0; }.admin-topbar { padding-inline: 24px; }.admin-content { padding-inline: 24px; } }
@media (max-width: 760px) { .admin-sidebar, .admin-collapse-toggle { display: none; }.admin-topbar { height: 53px; padding: 0 15px; }.mobile-menu-button { display: inline-flex; margin-right: 5px; }.admin-topbar-actions { gap: 8px; }.admin-search-trigger { min-width: 31px; width: 31px; justify-content: center; padding: 0; }.admin-search-trigger span, .admin-search-trigger kbd, .admin-topbar-action span, .admin-auth-link { display: none; }.admin-topbar-action { width: 31px; height: 31px; justify-content: center; padding: 0; border: 1px solid var(--admin-line-strong); border-radius: 6px; }.admin-content { padding: 24px 15px 45px; }.admin-page-heading { align-items: start; flex-direction: column; gap: 15px; margin-bottom: 22px; }.admin-page-heading h1 { font-size: 24px; }.admin-page-actions { width: 100%; }.admin-page-actions .ui-button { flex: 1; }.admin-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }.admin-metric-card { min-height: 102px; padding: 13px; }.admin-metric-card strong { margin-top: 13px; font-size: 21px; }.admin-section-card-header { padding-inline: 14px; }.health-list { grid-template-columns: repeat(2, 1fr); padding-inline: 14px; }.quota-summary { padding-inline: 14px; }.admin-table th, .admin-table td { padding-inline: 13px; }.admin-toolbar { flex-wrap: wrap; padding-inline: 13px; }.admin-search-field { min-width: 100%; max-width: none; }.admin-toolbar-note { margin-left: 0; }.admin-form-grid, .release-detail-grid, .json-summary-grid, .settings-placeholder-grid { grid-template-columns: 1fr; }.ui-dialog-content { width: min(100% - 22px, 520px); padding: 20px 17px; }.ui-dialog-content.wide-dialog { width: min(100% - 22px, 680px); }.admin-toast-viewport { right: 11px; bottom: 11px; max-width: calc(100vw - 22px); }.admin-toast { width: 100%; } } @media (max-width: 760px) { .admin-sidebar, .admin-collapse-toggle { display: none; }.admin-topbar { height: 53px; padding: 0 15px; }.mobile-menu-button { display: inline-flex; margin-right: 5px; }.admin-topbar-actions { gap: 8px; }.admin-search-trigger { min-width: 31px; width: 31px; justify-content: center; padding: 0; }.admin-search-trigger span, .admin-search-trigger kbd, .admin-topbar-action span, .admin-auth-link { display: none; }.admin-topbar-action { width: 31px; height: 31px; justify-content: center; padding: 0; border: 1px solid var(--admin-line-strong); border-radius: 6px; }.admin-content { padding: 24px 15px 45px; }.admin-page-heading { align-items: start; flex-direction: column; gap: 15px; margin-bottom: 22px; }.admin-page-heading h1 { font-size: 24px; }.admin-page-actions { width: 100%; }.admin-page-actions .ui-button { flex: 1; }.admin-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }.admin-metric-card { min-height: 102px; padding: 13px; }.admin-metric-card strong { margin-top: 13px; font-size: 21px; }.admin-section-card-header { padding-inline: 14px; }.health-list { grid-template-columns: repeat(2, 1fr); padding-inline: 14px; }.quota-summary { padding-inline: 14px; }.admin-table th, .admin-table td { padding-inline: 13px; }.admin-toolbar { flex-wrap: wrap; padding-inline: 13px; }.admin-search-field { min-width: 100%; max-width: none; }.admin-toolbar-note { margin-left: 0; }.admin-form-grid, .release-detail-grid, .json-summary-grid, .settings-placeholder-grid { grid-template-columns: 1fr; }.ui-dialog-content { width: min(100% - 22px, 520px); padding: 20px 17px; }.ui-dialog-content.wide-dialog { width: min(100% - 22px, 680px); }.admin-toast-viewport { right: 11px; bottom: 11px; max-width: calc(100vw - 22px); }.admin-toast { width: 100%; } }
+1 -1
View File
@@ -86,7 +86,7 @@ export function AdminShell({ children }: PropsWithChildren) {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [commandOpen, setCommandOpen] = useState(false); const [commandOpen, setCommandOpen] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const { notice, dismissNotice, dashboard } = useDashboard(); const { notice, dismissNotice } = useDashboard();
const { identity } = useIdentity(); const { identity } = useIdentity();
useEffect(() => { useEffect(() => {