Fix routing for json imports, added dynamic alerts.
This commit is contained in:
150
src/apps/Homepage/AlertCard.tsx
Normal file
150
src/apps/Homepage/AlertCard.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from "react";
|
||||
import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {X, ExternalLink, AlertTriangle, Info, CheckCircle, AlertCircle} from "lucide-react";
|
||||
|
||||
interface TermixAlert {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
expiresAt: string;
|
||||
priority?: 'low' | 'medium' | 'high' | 'critical';
|
||||
type?: 'info' | 'warning' | 'error' | 'success';
|
||||
actionUrl?: string;
|
||||
actionText?: string;
|
||||
}
|
||||
|
||||
interface AlertCardProps {
|
||||
alert: TermixAlert;
|
||||
onDismiss: (alertId: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const getAlertIcon = (type?: string) => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500"/>;
|
||||
case 'error':
|
||||
return <AlertCircle className="h-5 w-5 text-red-500"/>;
|
||||
case 'success':
|
||||
return <CheckCircle className="h-5 w-5 text-green-500"/>;
|
||||
case 'info':
|
||||
default:
|
||||
return <Info className="h-5 w-5 text-blue-500"/>;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBadgeVariant = (priority?: string) => {
|
||||
switch (priority) {
|
||||
case 'critical':
|
||||
return 'destructive';
|
||||
case 'high':
|
||||
return 'destructive';
|
||||
case 'medium':
|
||||
return 'secondary';
|
||||
case 'low':
|
||||
default:
|
||||
return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadgeVariant = (type?: string) => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'secondary';
|
||||
case 'error':
|
||||
return 'destructive';
|
||||
case 'success':
|
||||
return 'default';
|
||||
case 'info':
|
||||
default:
|
||||
return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
export function AlertCard({alert, onDismiss, onClose}: AlertCardProps): React.ReactElement {
|
||||
if (!alert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
onDismiss(alert.id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const formatExpiryDate = (expiryString: string) => {
|
||||
const expiryDate = new Date(expiryString);
|
||||
const now = new Date();
|
||||
const diffTime = expiryDate.getTime() - now.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < 0) return 'Expired';
|
||||
if (diffDays === 0) return 'Expires today';
|
||||
if (diffDays === 1) return 'Expires tomorrow';
|
||||
return `Expires in ${diffDays} days`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{getAlertIcon(alert.type)}
|
||||
<CardTitle className="text-xl font-bold">
|
||||
{alert.title}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4"/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{alert.priority && (
|
||||
<Badge variant={getPriorityBadgeVariant(alert.priority)}>
|
||||
{alert.priority.toUpperCase()}
|
||||
</Badge>
|
||||
)}
|
||||
{alert.type && (
|
||||
<Badge variant={getTypeBadgeVariant(alert.type)}>
|
||||
{alert.type}
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatExpiryDate(alert.expiresAt)}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-4">
|
||||
<p className="text-muted-foreground leading-relaxed whitespace-pre-wrap">
|
||||
{alert.message}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between pt-0">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
{alert.actionUrl && alert.actionText && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => window.open(alert.actionUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="gap-2"
|
||||
>
|
||||
{alert.actionText}
|
||||
<ExternalLink className="h-4 w-4"/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
202
src/apps/Homepage/AlertManager.tsx
Normal file
202
src/apps/Homepage/AlertManager.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {AlertCard} from "./AlertCard";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import axios from "axios";
|
||||
|
||||
interface TermixAlert {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
expiresAt: string;
|
||||
priority?: 'low' | 'medium' | 'high' | 'critical';
|
||||
type?: 'info' | 'warning' | 'error' | 'success';
|
||||
actionUrl?: string;
|
||||
actionText?: string;
|
||||
}
|
||||
|
||||
interface AlertManagerProps {
|
||||
userId: string | null;
|
||||
loggedIn: boolean;
|
||||
}
|
||||
|
||||
const apiBase = import.meta.env.DEV ? "http://localhost:8081/alerts" : "/alerts";
|
||||
|
||||
const API = axios.create({
|
||||
baseURL: apiBase,
|
||||
});
|
||||
|
||||
export function AlertManager({userId, loggedIn}: AlertManagerProps): React.ReactElement {
|
||||
const [alerts, setAlerts] = useState<TermixAlert[]>([]);
|
||||
const [currentAlertIndex, setCurrentAlertIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (loggedIn && userId) {
|
||||
fetchUserAlerts();
|
||||
}
|
||||
}, [loggedIn, userId]);
|
||||
|
||||
const fetchUserAlerts = async () => {
|
||||
if (!userId) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await API.get(`/user/${userId}`);
|
||||
|
||||
const userAlerts = response.data.alerts || [];
|
||||
|
||||
const sortedAlerts = userAlerts.sort((a: TermixAlert, b: TermixAlert) => {
|
||||
const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||
const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] || 0;
|
||||
const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] || 0;
|
||||
|
||||
if (aPriority !== bPriority) {
|
||||
return bPriority - aPriority;
|
||||
}
|
||||
|
||||
return new Date(a.expiresAt).getTime() - new Date(b.expiresAt).getTime();
|
||||
});
|
||||
|
||||
setAlerts(sortedAlerts);
|
||||
setCurrentAlertIndex(0);
|
||||
} catch (err) {
|
||||
setError('Failed to load alerts');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismissAlert = async (alertId: string) => {
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
const response = await API.post('/dismiss', {
|
||||
userId,
|
||||
alertId
|
||||
});
|
||||
|
||||
setAlerts(prev => {
|
||||
const newAlerts = prev.filter(alert => alert.id !== alertId);
|
||||
return newAlerts;
|
||||
});
|
||||
|
||||
setCurrentAlertIndex(prevIndex => {
|
||||
const newAlertsLength = alerts.length - 1;
|
||||
if (newAlertsLength === 0) return 0;
|
||||
if (prevIndex >= newAlertsLength) return Math.max(0, newAlertsLength - 1);
|
||||
return prevIndex;
|
||||
});
|
||||
} catch (err) {
|
||||
setError('Failed to dismiss alert');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseCurrentAlert = () => {
|
||||
if (alerts.length === 0) return;
|
||||
|
||||
if (currentAlertIndex < alerts.length - 1) {
|
||||
setCurrentAlertIndex(currentAlertIndex + 1);
|
||||
} else {
|
||||
setAlerts([]);
|
||||
setCurrentAlertIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviousAlert = () => {
|
||||
if (currentAlertIndex > 0) {
|
||||
setCurrentAlertIndex(currentAlertIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextAlert = () => {
|
||||
if (currentAlertIndex < alerts.length - 1) {
|
||||
setCurrentAlertIndex(currentAlertIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
if (!loggedIn || !userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<div className="bg-background border rounded-lg p-3 shadow-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||
<span className="text-sm text-muted-foreground">Loading alerts...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentAlert = alerts[currentAlertIndex];
|
||||
|
||||
if (!currentAlert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const priorityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
||||
alerts.forEach(alert => {
|
||||
const priority = alert.priority || 'low';
|
||||
priorityCounts[priority as keyof typeof priorityCounts]++;
|
||||
});
|
||||
const hasMultipleAlerts = alerts.length > 1;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-10">
|
||||
<div className="relative w-full max-w-2xl mx-4">
|
||||
{/* Current Alert */}
|
||||
<AlertCard
|
||||
alert={currentAlert}
|
||||
onDismiss={handleDismissAlert}
|
||||
onClose={handleCloseCurrentAlert}
|
||||
/>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
{hasMultipleAlerts && (
|
||||
<div className="absolute -bottom-16 left-1/2 transform -translate-x-1/2 flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handlePreviousAlert}
|
||||
disabled={currentAlertIndex === 0}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{currentAlertIndex + 1} of {alerts.length}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleNextAlert}
|
||||
disabled={currentAlertIndex === alerts.length - 1}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="absolute -bottom-20 left-1/2 transform -translate-x-1/2">
|
||||
<div className="bg-destructive text-destructive-foreground px-3 py-1 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,17 +3,12 @@ import React, {useEffect, useState} from "react";
|
||||
import {HomepageAuth} from "@/apps/Homepage/HomepageAuth.tsx";
|
||||
import axios from "axios";
|
||||
import {HomepageUpdateLog} from "@/apps/Homepage/HompageUpdateLog.tsx";
|
||||
import {HomepageWelcomeCard} from "@/apps/Homepage/HomepageWelcomeCard.tsx";
|
||||
import {AlertManager} from "@/apps/Homepage/AlertManager.tsx";
|
||||
|
||||
interface HomepageProps {
|
||||
onSelectView: (view: string) => void;
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, days = 7) {
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
return document.cookie.split('; ').reduce((r, v) => {
|
||||
const parts = v.split('=');
|
||||
@@ -21,6 +16,11 @@ function getCookie(name: string) {
|
||||
}, "");
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, days = 7) {
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
||||
}
|
||||
|
||||
const apiBase = import.meta.env.DEV ? "http://localhost:8081/users" : "/users";
|
||||
|
||||
const API = axios.create({
|
||||
@@ -31,13 +31,12 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [dbError, setDbError] = useState<string | null>(null);
|
||||
const [showWelcomeCard, setShowWelcomeCard] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const jwt = getCookie("jwt");
|
||||
const welcomeHidden = getCookie("welcome_hidden");
|
||||
|
||||
if (jwt) {
|
||||
setAuthLoading(true);
|
||||
@@ -49,13 +48,14 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.data.is_admin);
|
||||
setUsername(meRes.data.username || null);
|
||||
setUserId(meRes.data.userId || null);
|
||||
setDbError(null);
|
||||
setShowWelcomeCard(welcomeHidden !== "true");
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
setDbError("Could not connect to the database. Please try again later.");
|
||||
@@ -69,11 +69,6 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleHideWelcomeCard = () => {
|
||||
setShowWelcomeCard(false);
|
||||
setCookie("welcome_hidden", "true", 365 * 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<HomepageSidebar
|
||||
onSelectView={onSelectView}
|
||||
@@ -87,6 +82,7 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
||||
setLoggedIn={setLoggedIn}
|
||||
setIsAdmin={setIsAdmin}
|
||||
setUsername={setUsername}
|
||||
setUserId={setUserId}
|
||||
loggedIn={loggedIn}
|
||||
authLoading={authLoading}
|
||||
dbError={dbError}
|
||||
@@ -97,12 +93,11 @@ export function Homepage({onSelectView}: HomepageProps): React.ReactElement {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loggedIn && !authLoading && showWelcomeCard && (
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-10">
|
||||
<HomepageWelcomeCard onHidePermanently={handleHideWelcomeCard}/>
|
||||
</div>
|
||||
)}
|
||||
{/* Alert Manager - replaces the old welcome card */}
|
||||
<AlertManager
|
||||
userId={userId}
|
||||
loggedIn={loggedIn}
|
||||
/>
|
||||
</div>
|
||||
</HomepageSidebar>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ interface HomepageAuthProps extends React.ComponentProps<"div"> {
|
||||
setLoggedIn: (loggedIn: boolean) => void;
|
||||
setIsAdmin: (isAdmin: boolean) => void;
|
||||
setUsername: (username: string | null) => void;
|
||||
setUserId: (userId: string | null) => void;
|
||||
loggedIn: boolean;
|
||||
authLoading: boolean;
|
||||
dbError: string | null;
|
||||
@@ -40,6 +41,7 @@ export function HomepageAuth({
|
||||
setLoggedIn,
|
||||
setIsAdmin,
|
||||
setUsername,
|
||||
setUserId,
|
||||
loggedIn,
|
||||
authLoading,
|
||||
dbError,
|
||||
@@ -144,6 +146,7 @@ export function HomepageAuth({
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.data.is_admin);
|
||||
setUsername(meRes.data.username || null);
|
||||
setUserId(meRes.data.id || null);
|
||||
setDbError(null);
|
||||
if (tab === "signup") {
|
||||
setSignupConfirmPassword("");
|
||||
@@ -154,6 +157,7 @@ export function HomepageAuth({
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
setDbError("Could not connect to the database. Please try again later.");
|
||||
@@ -298,6 +302,7 @@ export function HomepageAuth({
|
||||
setLoggedIn(true);
|
||||
setIsAdmin(!!meRes.data.is_admin);
|
||||
setUsername(meRes.data.username || null);
|
||||
setUserId(meRes.data.id || null);
|
||||
setDbError(null);
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
})
|
||||
@@ -307,6 +312,7 @@ export function HomepageAuth({
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
setCookie("jwt", "", -1);
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
})
|
||||
|
||||
@@ -119,23 +119,37 @@ export function HomepageSidebar({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (adminSheetOpen) {
|
||||
API.get("/registration-allowed").then(res => {
|
||||
setAllowRegistration(res.data.allowed);
|
||||
});
|
||||
|
||||
API.get("/oidc-config").then(res => {
|
||||
if (res.data) {
|
||||
setOidcConfig(res.data);
|
||||
}
|
||||
}).catch((error) => {
|
||||
});
|
||||
fetchUsers();
|
||||
const jwt = getCookie("jwt");
|
||||
if (jwt && isAdmin) {
|
||||
API.get("/oidc-config").then(res => {
|
||||
if (res.data) {
|
||||
setOidcConfig(res.data);
|
||||
}
|
||||
}).catch((error) => {
|
||||
});
|
||||
fetchUsers();
|
||||
}
|
||||
} else {
|
||||
fetchAdminCount();
|
||||
const jwt = getCookie("jwt");
|
||||
if (jwt && isAdmin) {
|
||||
fetchAdminCount();
|
||||
}
|
||||
}
|
||||
}, [adminSheetOpen]);
|
||||
}, [adminSheetOpen, isAdmin]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
setAdminSheetOpen(false);
|
||||
setUsers([]);
|
||||
setAdminCount(0);
|
||||
}
|
||||
}, [isAdmin]);
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRegLoading(true);
|
||||
const jwt = getCookie("jwt");
|
||||
try {
|
||||
@@ -153,6 +167,11 @@ export function HomepageSidebar({
|
||||
|
||||
const handleOIDCConfigSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOidcLoading(true);
|
||||
setOidcError(null);
|
||||
setOidcSuccess(null);
|
||||
@@ -214,8 +233,13 @@ export function HomepageSidebar({
|
||||
};
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setUsersLoading(true);
|
||||
const jwt = getCookie("jwt");
|
||||
|
||||
if (!jwt || !isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUsersLoading(true);
|
||||
try {
|
||||
const response = await API.get("/list", {
|
||||
headers: {Authorization: `Bearer ${jwt}`}
|
||||
@@ -233,6 +257,11 @@ export function HomepageSidebar({
|
||||
|
||||
const fetchAdminCount = async () => {
|
||||
const jwt = getCookie("jwt");
|
||||
|
||||
if (!jwt || !isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await API.get("/list", {
|
||||
headers: {Authorization: `Bearer ${jwt}`}
|
||||
@@ -248,6 +277,10 @@ export function HomepageSidebar({
|
||||
e.preventDefault();
|
||||
if (!newAdminUsername.trim()) return;
|
||||
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMakeAdminLoading(true);
|
||||
setMakeAdminError(null);
|
||||
setMakeAdminSuccess(null);
|
||||
@@ -271,6 +304,10 @@ export function HomepageSidebar({
|
||||
const removeAdminStatus = async (username: string) => {
|
||||
if (!confirm(`Are you sure you want to remove admin status from ${username}?`)) return;
|
||||
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jwt = getCookie("jwt");
|
||||
try {
|
||||
await API.post("/remove-admin",
|
||||
@@ -286,6 +323,10 @@ export function HomepageSidebar({
|
||||
const deleteUser = async (username: string) => {
|
||||
if (!confirm(`Are you sure you want to delete user ${username}? This action cannot be undone.`)) return;
|
||||
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jwt = getCookie("jwt");
|
||||
try {
|
||||
await API.delete("/delete-user", {
|
||||
@@ -375,7 +416,11 @@ export function HomepageSidebar({
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
|
||||
onSelect={() => setAdminSheetOpen(true)}>
|
||||
onSelect={() => {
|
||||
if (isAdmin) {
|
||||
setAdminSheetOpen(true);
|
||||
}
|
||||
}}>
|
||||
<span>Admin Settings</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -400,9 +445,12 @@ export function HomepageSidebar({
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
{/* Admin Settings Sheet (always rendered, only openable if isAdmin) */}
|
||||
{/* Admin Settings Sheet */}
|
||||
{isAdmin && (
|
||||
<Sheet open={adminSheetOpen} onOpenChange={setAdminSheetOpen}>
|
||||
<Sheet open={adminSheetOpen && isAdmin} onOpenChange={(open) => {
|
||||
if (open && !isAdmin) return;
|
||||
setAdminSheetOpen(open);
|
||||
}}>
|
||||
<SheetContent side="left" className="w-[700px] max-h-screen overflow-y-auto">
|
||||
<SheetHeader className="px-6 pb-4">
|
||||
<SheetTitle>Admin Settings</SheetTitle>
|
||||
@@ -510,7 +558,7 @@ export function HomepageSidebar({
|
||||
id="token_url"
|
||||
value={oidcConfig.token_url}
|
||||
onChange={(e) => handleOIDCConfigChange('token_url', e.target.value)}
|
||||
placeholder="http://100.98.3.50:9000/application/o/token/"
|
||||
placeholder="https://your-provider.com/application/o/token/"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {Button} from "@/components/ui/button";
|
||||
|
||||
interface HomepageWelcomeCardProps {
|
||||
onHidePermanently: () => void;
|
||||
}
|
||||
|
||||
export function HomepageWelcomeCard({onHidePermanently}: HomepageWelcomeCardProps): React.ReactElement {
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold text-center">
|
||||
The Future of Termix
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-center leading-relaxed">
|
||||
Please checkout the linked survey{" "}
|
||||
<a
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSeGvnQODFtnpjmJsMKgASbaQ87CLQEBCcnzK_Vuw5TdfbfIyA/viewform?usp=sharing&ouid=107601685503825301492"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline hover:text-primary/80 transition-colors"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
. The purpose of this survey is to gather feedback from users on what the future UI of Termix could
|
||||
look like to optimize server management. Please take a minute or two to read the survey questions
|
||||
and answer them to the best of your ability. Thank you!
|
||||
</p>
|
||||
<p className="text-muted-foreground text-center leading-relaxed mt-6">
|
||||
A special thanks to those in Asia who recently joined Termix through various forum posts, keep
|
||||
sharing it! A Chinese translation is planned for Termix, but since I don’t speak Chinese, I’ll need
|
||||
to hire someone to help with the translation. If you’d like to support me financially, you can do
|
||||
so{" "}
|
||||
<a
|
||||
href="https://github.com/sponsors/LukeGus"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline hover:text-primary/80 transition-colors"
|
||||
>
|
||||
here.
|
||||
</a>
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onHidePermanently}
|
||||
className="w-full max-w-xs"
|
||||
>
|
||||
Hide Permanently
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -96,21 +96,21 @@ interface ConfigEditorShortcut {
|
||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
|
||||
const sshHostApi = axios.create({
|
||||
baseURL: isLocalhost ? 'http://localhost:8081' : window.location.origin,
|
||||
baseURL: isLocalhost ? 'http://localhost:8081' : '',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const tunnelApi = axios.create({
|
||||
baseURL: isLocalhost ? 'http://localhost:8083' : window.location.origin,
|
||||
baseURL: isLocalhost ? 'http://localhost:8083' : '',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const configEditorApi = axios.create({
|
||||
baseURL: isLocalhost ? 'http://localhost:8084' : window.location.origin,
|
||||
baseURL: isLocalhost ? 'http://localhost:8084' : '',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user