Dashboard Release 1.0
This commit is contained in:
1 parent
033fb87269
commit
ed57dc2d4f
8 files changed
+143
-47
No files matched your search
@@ -584,6 +584,11 @@ export const api = {
|
||||
{ method: "POST", body: JSON.stringify(body) },
|
||||
),
|
||||
|
||||
resetDashboard: () =>
|
||||
request<ApiResponse<unknown>>("/api/dashboard/setup/reset", {
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
completeSetup: (body: Record<string, unknown>) =>
|
||||
request<
|
||||
ApiResponse<{ setupComplete: boolean; botRestart?: BotRestartResult }>
|
||||
|
||||
@@ -80,8 +80,8 @@ const sections: {
|
||||
{ title: "Ticket Panel", url: "/configs?file=ticket-panel", icon: Ticket01Icon },
|
||||
{ title: "AI Assistant", url: "/configs?file=supportbot-ai", icon: AiBrain01Icon },
|
||||
{ title: "Messages", url: "/configs?file=messages", icon: Message01Icon },
|
||||
{ title: "Commands", url: "/configs?file=commands", icon: CommandIcon },
|
||||
{ title: "Staff Roles", url: "/configs?file=supportbot&tab=Roles", icon: UserMultiple02Icon },
|
||||
{ title: "Commands", url: "/configs?file=commands", icon: CommandIcon },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,15 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useBranding } from "@/context/BrandingContext";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
function readFileAsDataUrl(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -20,22 +29,23 @@ interface DashboardBrandingSettingsProps {
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export function DashboardBrandingDangerZone({ canEdit }: DashboardBrandingSettingsProps) {
|
||||
const { setBrandingLocal } = useBranding();
|
||||
export function DashboardDangerZone({ canEdit }: DashboardBrandingSettingsProps) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
async function resetAll() {
|
||||
async function resetDashboard() {
|
||||
if (!canEdit) return;
|
||||
setSaving(true);
|
||||
setMessage("");
|
||||
try {
|
||||
const res = await api.resetBranding();
|
||||
if (res.data) setBrandingLocal(res.data);
|
||||
setMessage(res.message || "Reset to defaults.");
|
||||
const res = await api.resetDashboard();
|
||||
setMessage(res.message || "Dashboard reset successfully.");
|
||||
setTimeout(() => {
|
||||
window.location.href = "/setup";
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
setMessage(err instanceof Error ? err.message : "Reset failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
@@ -45,17 +55,34 @@ export function DashboardBrandingDangerZone({ canEdit }: DashboardBrandingSettin
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Reset all branding text and favicon to their original defaults.
|
||||
Reset the dashboard to factory settings. This clears the configuration and returns you to the initial setup screen.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={saving}
|
||||
onClick={() => void resetAll()}
|
||||
>
|
||||
Reset Branding to Default
|
||||
</Button>
|
||||
{message && <p className="text-xs text-muted-foreground">{message}</p>}
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="destructive">
|
||||
Reset Dashboard
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will reset the dashboard to setup mode.
|
||||
You will need to reconnect the Discord OAuth and reconfigure the dashboard owners.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{message && <p className="text-sm font-medium text-destructive">{message}</p>}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => void resetDashboard()} disabled={saving}>
|
||||
{saving ? "Resetting..." : "Yes, reset dashboard"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -126,13 +153,6 @@ export function DashboardBrandingSettings({ canEdit }: DashboardBrandingSettings
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textDirty =
|
||||
title.trim() !== branding.title || pageTitle.trim() !== branding.pageTitle;
|
||||
|
||||
@@ -253,8 +273,6 @@ export function DashboardBrandingSettings({ canEdit }: DashboardBrandingSettings
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export const SETUP_STEP_IDS = [
|
||||
"oauth",
|
||||
"redirectUri",
|
||||
"owners",
|
||||
"emeraldApi",
|
||||
] as const;
|
||||
|
||||
export type SetupStepId = (typeof SETUP_STEP_IDS)[number];
|
||||
@@ -66,6 +67,12 @@ export const SETUP_STEPS: {
|
||||
subtitle: "Who can access this panel",
|
||||
icon: UserMultiple02Icon,
|
||||
},
|
||||
{
|
||||
id: "emeraldApi",
|
||||
title: "Emerald API",
|
||||
subtitle: "One-click addons",
|
||||
icon: Key01Icon,
|
||||
},
|
||||
];
|
||||
|
||||
function validationKeyForStep(id: SetupStepId): string {
|
||||
@@ -636,3 +643,56 @@ export function SetupNav({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmeraldApiKeyStep({
|
||||
values,
|
||||
setValues,
|
||||
validation,
|
||||
validating,
|
||||
onValidate,
|
||||
}: {
|
||||
values: SetupFormValues;
|
||||
setValues: (v: SetupFormValues) => void;
|
||||
validation: Record<string, FieldValidationResult>;
|
||||
validating: boolean;
|
||||
onValidate: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-5">
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
An Emerald Services API key is required to use the one-click addon installer in the dashboard. You can get yours from the{" "}
|
||||
<a
|
||||
href="https://emeraldsrv.dev"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
Emerald Services Marketplace
|
||||
</a>.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="emeraldApiKey">Emerald API key</Label>
|
||||
<Input
|
||||
id="emeraldApiKey"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={values.emeraldApiKey}
|
||||
onChange={(e) => setValues({ ...values, emeraldApiKey: e.target.value })}
|
||||
placeholder="em_..."
|
||||
className="h-11 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<ValidationBanner result={validation.emeraldApi} />
|
||||
<div className="mt-auto">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={validating || !values.emeraldApiKey}
|
||||
onClick={onValidate}
|
||||
>
|
||||
{validating ? "Checking…" : "Save key"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,4 +35,5 @@ export interface SetupFormValues {
|
||||
clientSecret: string;
|
||||
redirectUri: string;
|
||||
ownerUserIds: string;
|
||||
emeraldApiKey: string;
|
||||
}
|
||||
@@ -83,24 +83,16 @@ export default function Addons() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Addons</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Install addons from the{" "}
|
||||
Requires an API key from{" "}
|
||||
<a
|
||||
href={repository || "https://emeraldsrv.dev"}
|
||||
href="https://emeraldsrv.dev"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Emerald Services Marketplace
|
||||
</a>
|
||||
. Enable addons under{" "}
|
||||
<Link
|
||||
to="/configs?file=supportbot"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
Bot config → General → Addons
|
||||
</Link>
|
||||
. Installed addon configs appear in the left sidebar under{" "}
|
||||
<strong className="font-medium text-foreground">Addons</strong>.
|
||||
emeraldsrv.dev
|
||||
</a>{" "}
|
||||
to browse and install addons. Configs will appear in the sidebar once installed.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
saveAccentTheme,
|
||||
} from "@/lib/accent-theme";
|
||||
import { normalizeHex } from "@/lib/normalize-hex";
|
||||
import { DashboardBrandingSettings, DashboardBrandingDangerZone } from "@/components/settings/dashboard-branding-settings";
|
||||
import { DashboardBrandingSettings, DashboardDangerZone } from "@/components/settings/dashboard-branding-settings";
|
||||
import { DashboardApiSettings } from "@/components/settings/dashboard-api-settings";
|
||||
|
||||
export default function Settings() {
|
||||
@@ -42,7 +42,7 @@ export default function Settings() {
|
||||
() => getStoredAccentHex() ?? DEFAULT_ACCENT_HEX,
|
||||
);
|
||||
const [accentSaved, setAccentSaved] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<"appearance" | "api" | "updates">("appearance");
|
||||
const [activeTab, setActiveTab] = useState<"appearance" | "api" | "updates" | "danger">("appearance");
|
||||
|
||||
const loadCheck = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -113,6 +113,15 @@ export default function Settings() {
|
||||
>
|
||||
Update Checker
|
||||
</Button>
|
||||
{canUpdate && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`justify-start w-full text-destructive hover:bg-destructive/10 hover:text-destructive ${activeTab === "danger" ? "bg-destructive/10 border-l-2 border-destructive rounded-l-none" : ""}`}
|
||||
onClick={() => setActiveTab("danger")}
|
||||
>
|
||||
Danger Zone
|
||||
</Button>
|
||||
)}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -190,19 +199,22 @@ export default function Settings() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canUpdate && (
|
||||
{activeTab === "danger" && canUpdate && (
|
||||
<Card className="border-destructive/50 bg-destructive/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg text-destructive">Danger Zone</CardTitle>
|
||||
<CardDescription className="text-destructive/80">
|
||||
Destructive actions that reset or permanently alter your dashboard.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DashboardBrandingDangerZone canEdit={canUpdate} />
|
||||
<DashboardDangerZone canEdit={canUpdate} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "api" && (
|
||||
<Card className="border-border bg-card">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SetupProgress,
|
||||
SetupStepCard,
|
||||
SetupWizardShell,
|
||||
EmeraldApiKeyStep,
|
||||
type SetupStepId,
|
||||
} from "@/components/setup/setup-wizard";
|
||||
import { useBranding } from "@/context/BrandingContext";
|
||||
@@ -37,6 +38,7 @@ function parseOwnerIds(raw: string) {
|
||||
function validationKeyForStep(id: SetupStepId): string {
|
||||
if (id === "oauth") return "oauth";
|
||||
if (id === "owners") return "owners";
|
||||
if (id === "emeraldApi") return "emeraldApi";
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -53,6 +55,7 @@ export default function Setup() {
|
||||
clientSecret: "",
|
||||
redirectUri: "",
|
||||
ownerUserIds: "",
|
||||
emeraldApiKey: "",
|
||||
});
|
||||
const [validation, setValidation] = useState<Record<string, FieldValidationResult>>({});
|
||||
const [validating, setValidating] = useState(false);
|
||||
@@ -76,6 +79,7 @@ export default function Setup() {
|
||||
clientSecret: values.clientSecret,
|
||||
redirectUri: values.redirectUri,
|
||||
ownerUserIds: parseOwnerIds(values.ownerUserIds),
|
||||
emeraldApiKey: values.emeraldApiKey,
|
||||
}),
|
||||
[values],
|
||||
);
|
||||
@@ -163,6 +167,10 @@ export default function Setup() {
|
||||
return (
|
||||
<OwnersStep {...common} onValidate={() => void validateCurrentStep()} />
|
||||
);
|
||||
case "emeraldApi":
|
||||
return (
|
||||
<EmeraldApiKeyStep {...common} onValidate={() => void validateCurrentStep()} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +192,7 @@ export default function Setup() {
|
||||
<SetupWizardShell
|
||||
brandingTitle={branding.title}
|
||||
faviconUrl={branding.faviconUrl}
|
||||
faviconVersion={branding.updatedAt}
|
||||
faviconVersion={branding.updatedAt ?? undefined}
|
||||
hasFavicon={branding.hasFavicon}
|
||||
>
|
||||
{statusError ? (
|
||||
|
||||
Reference in new issue
Block a user