Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app-next/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,12 @@
},
"auth": {
"recommended": "EMPFOHLEN",
"signInRequired": {
"uploadDataset": "Sie müssen sich anmelden, um einen Datensatz hochzuladen",
"createTask": "Sie müssen sich anmelden, um eine Aufgabe zu definieren",
"createCollection": "Sie müssen sich anmelden, um eine Sammlung zu erstellen",
"default": "Sie müssen sich anmelden, um fortzufahren"
},
"signIn": {
"title": "Anmelden - OpenML",
"description": "Melden Sie sich bei Ihrem OpenML-Konto an",
Expand Down
6 changes: 6 additions & 0 deletions app-next/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,12 @@
},
"auth": {
"recommended": "RECOMMENDED",
"signInRequired": {
"uploadDataset": "You need to sign in to upload a dataset",
"createTask": "You need to sign in to define a task",
"createCollection": "You need to sign in to create a collection",
"default": "You need to sign in to continue"
},
"signIn": {
"title": "Sign In - OpenML",
"description": "Sign in to your OpenML account",
Expand Down
6 changes: 6 additions & 0 deletions app-next/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,12 @@
},
"auth": {
"recommended": "RECOMMANDÉ",
"signInRequired": {
"uploadDataset": "Vous devez vous connecter pour télécharger un jeu de données",
"createTask": "Vous devez vous connecter pour définir une tâche",
"createCollection": "Vous devez vous connecter pour créer une collection",
"default": "Vous devez vous connecter pour continuer"
},
"signIn": {
"title": "Se connecter - OpenML",
"description": "Connectez-vous à votre compte OpenML",
Expand Down
6 changes: 6 additions & 0 deletions app-next/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,12 @@
},
"auth": {
"recommended": "AANBEVOLEN",
"signInRequired": {
"uploadDataset": "U moet inloggen om een dataset te uploaden",
"createTask": "U moet inloggen om een taak te definiëren",
"createCollection": "U moet inloggen om een collectie aan te maken",
"default": "U moet inloggen om verder te gaan"
},
"signIn": {
"title": "Inloggen - OpenML",
"description": "Log in op uw OpenML-account",
Expand Down
26 changes: 26 additions & 0 deletions app-next/src/app/[locale]/(explore)/collections/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";

export default async function CollectionCreatePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const session = await getServerSession(authOptions);

if (!session) {
redirect(
`/${locale}/auth/sign-in?reason=createCollection&callbackUrl=/${locale}/collections/create`
);
}

// TODO: Implement collection creation form
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-3xl font-bold">Create Collection</h1>
<p className="text-muted-foreground mt-2">Coming soon.</p>
</div>
);
}
10 changes: 9 additions & 1 deletion app-next/src/app/[locale]/(explore)/datasets/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default async function DatasetEditPage({
// Auth check — redirect to sign-in if not authenticated
const session = await getServerSession(authOptions);
if (!session?.user) {
redirect(`/auth/signin?callbackUrl=/datasets/${id}/edit`);
redirect(`/${locale}/auth/sign-in?reason=uploadDataset&callbackUrl=/${locale}/datasets/${id}/edit`);
}

const dataset = await fetchDataset(id);
Expand All @@ -39,12 +39,20 @@ export default async function DatasetEditPage({
const userId = (session.user as { id?: string }).id;
const isOwner = userId ? Number(userId) === dataset.uploader_id : false;

// Check whether the session has a valid OpenML API key
const hasApiKey = !!(session as { apikey?: string }).apikey;
// OAuth users created in local dev environments don't have a real OpenML API key
const isLocalUser =
(session.user as { isLocalUser?: boolean }).isLocalUser ?? false;

return (
<div className="container mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
<DatasetEditForm
datasetId={dataset.data_id}
datasetName={dataset.name}
isOwner={isOwner}
hasApiKey={hasApiKey}
isLocalUser={isLocalUser}
initialValues={{
description: dataset.description || "",
creator: dataset.creator || "",
Expand Down
26 changes: 26 additions & 0 deletions app-next/src/app/[locale]/(explore)/datasets/upload/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";

export default async function DatasetUploadPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const session = await getServerSession(authOptions);

if (!session) {
redirect(
`/${locale}/auth/sign-in?reason=uploadDataset&callbackUrl=/${locale}/datasets/upload`
);
}

// TODO: Implement dataset upload form
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-3xl font-bold">Upload Dataset</h1>
<p className="text-muted-foreground mt-2">Coming soon.</p>
</div>
);
}
26 changes: 26 additions & 0 deletions app-next/src/app/[locale]/(explore)/tasks/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";

export default async function TaskCreatePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const session = await getServerSession(authOptions);

if (!session) {
redirect(
`/${locale}/auth/sign-in?reason=createTask&callbackUrl=/${locale}/tasks/create`
);
}

// TODO: Implement task creation form
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-3xl font-bold">Define Task</h1>
<p className="text-muted-foreground mt-2">Coming soon.</p>
</div>
);
}
49 changes: 36 additions & 13 deletions app-next/src/app/[locale]/(extra)/auth/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { LogIn } from "lucide-react";

export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("auth");
Expand All @@ -20,28 +21,50 @@ export async function generateMetadata(): Promise<Metadata> {

export default async function SignInPage({
params,
searchParams,
}: {
params: Promise<{ locale: string }>;
searchParams: Promise<{ reason?: string }>;
}) {
const { locale } = await params;
const { reason } = await searchParams;
setRequestLocale(locale);
const t = await getTranslations("auth");

// Map reason param to translated message
const reasonMessages: Record<string, string> = {
uploadDataset: t("signInRequired.uploadDataset"),
createTask: t("signInRequired.createTask"),
createCollection: t("signInRequired.createCollection"),
};
const reasonMessage = reason
? (reasonMessages[reason] ?? t("signInRequired.default"))
: null;

return (
<div className="container mx-auto flex min-h-[calc(100vh-200px)] items-center justify-center px-4 py-16">
<Card className="w-full max-w-md border-slate-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-800">
<CardHeader className="text-center">
<CardTitle className="text-3xl text-slate-800 dark:text-white">
{t("signIn.welcome")}
</CardTitle>
<CardDescription className="text-base text-slate-500 dark:text-slate-400">
{t("signIn.subtitle")}
</CardDescription>
</CardHeader>
<CardContent>
<AuthTabs />
</CardContent>
</Card>
<div className="w-full max-w-md space-y-4">
{/* Contextual message shown when redirected from a protected action */}
{reasonMessage && (
<div className="flex items-center gap-3 rounded-lg border border-slate-200 bg-slate-800 px-4 py-3 text-slate-200 dark:border-slate-950 dark:bg-slate-950 dark:text-slate-200">
<LogIn className="size-5 shrink-0" />
<p className="text-sm font-medium">{reasonMessage}</p>
</div>
)}
<Card className="border-slate-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-800">
<CardHeader className="text-center">
<CardTitle className="text-3xl text-slate-800 dark:text-white">
{t("signIn.welcome")}
</CardTitle>
<CardDescription className="text-base text-slate-600 dark:text-slate-300">
{t("signIn.subtitle")}
</CardDescription>
</CardHeader>
<CardContent>
<AuthTabs />
</CardContent>
</Card>
</div>
</div>
);
}
6 changes: 5 additions & 1 deletion app-next/src/app/api/datasets/[id]/edit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@ export async function POST(
if (!response.ok) {
const text = await response.text();
console.error(`OpenML API error editing dataset ${id}:`, text);
const message =
response.status === 401 || response.status === 403
? "Your API key is not accepted by the OpenML server. If you are using a local test account, dataset editing is not supported — only real OpenML accounts can save changes."
: "Failed to save changes. Please try again.";
return NextResponse.json(
{ error: "Failed to save changes. Please try again." },
{ error: message },
{ status: response.status },
);
}
Expand Down
57 changes: 34 additions & 23 deletions app-next/src/components/dataset/dataset-edit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useLocale } from "next-intl";
import Link from "next/link";
import { ArrowLeft, Save, Loader2, AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";

interface DatasetEditFormProps {
datasetId: number;
datasetName: string;
isOwner: boolean;
hasApiKey: boolean;
isLocalUser: boolean;
initialValues: {
description: string;
creator: string;
Expand All @@ -33,29 +37,27 @@ export function DatasetEditForm({
datasetId,
datasetName,
isOwner,
hasApiKey,
isLocalUser,
initialValues,
features,
}: DatasetEditFormProps) {
const router = useRouter();
const locale = useLocale();
const { toast } = useToast();
const [values, setValues] = useState(initialValues);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);

const handleChange = (
field: keyof typeof values,
value: string,
) => {
setValues((prev) => ({ ...prev, [field]: value }));
setError(null);
setSuccess(false);
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setError(null);
setSuccess(false);

try {
const res = await fetch(`/api/datasets/${datasetId}/edit`, {
Expand All @@ -72,14 +74,21 @@ export function DatasetEditForm({
throw new Error(data.error || `Failed to save (${res.status})`);
}

setSuccess(true);
// Redirect back to dataset page after short delay
toast({
title: "Changes saved",
description: "Redirecting back to dataset...",
});

setTimeout(() => {
router.push(`/datasets/${datasetId}`);
router.push(`/${locale}/datasets/${datasetId}`);
router.refresh();
}, 1500);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save changes");
toast({
title: "Failed to save",
description: err instanceof Error ? err.message : "Failed to save changes",
variant: "destructive",
});
} finally {
setSaving(false);
}
Expand All @@ -90,7 +99,7 @@ export function DatasetEditForm({
{/* Header */}
<div className="mb-8">
<Link
href={`/datasets/${datasetId}`}
href={`/${locale}/datasets/${datasetId}`}
className="text-muted-foreground hover:text-foreground mb-4 inline-flex items-center gap-1.5 text-sm transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Expand All @@ -103,16 +112,18 @@ export function DatasetEditForm({
</p>
</div>

{/* Status messages */}
{error && (
<div className="mb-6 flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-400">
<AlertTriangle className="h-4 w-4 shrink-0" />
{error}
</div>
)}
{success && (
<div className="mb-6 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700 dark:border-green-900/50 dark:bg-green-950/30 dark:text-green-400">
Changes saved successfully! Redirecting...
{/* Warning: user has no valid OpenML API key (e.g. local dev account) */}
{(!hasApiKey || isLocalUser) && (
<div className="mb-6 flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-300">
<AlertTriangle className="mt-0.5 size-5 shrink-0" />
<div className="space-y-1">
<p className="text-sm font-medium">Saving is unavailable in this environment</p>
<p className="text-xs">
{isLocalUser
? "This account was created locally and does not have a valid OpenML API key. Dataset edits cannot be saved to the OpenML backend in a local development environment."
: "Your session does not include an OpenML API key. Saving changes requires signing in with a valid OpenML account."}
</p>
</div>
</div>
)}

Expand Down Expand Up @@ -284,12 +295,12 @@ export function DatasetEditForm({

{/* Actions */}
<div className="flex items-center justify-between">
<Link href={`/datasets/${datasetId}`}>
<Link href={`/${locale}/datasets/${datasetId}`}>
<Button type="button" variant="outline">
Cancel
</Button>
</Link>
<Button type="submit" disabled={saving} className="gap-2">
<Button type="submit" disabled={saving || !hasApiKey || isLocalUser} className="gap-2" title={(!hasApiKey || isLocalUser) ? "Saving is not available in this environment" : undefined}>
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Expand Down