+>(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/app-next/src/components/ui/tabs.tsx b/app-next/src/components/ui/tabs.tsx
new file mode 100644
index 00000000..4ac19902
--- /dev/null
+++ b/app-next/src/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+
+import { cn } from "@/lib/utils";
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/app-next/src/components/ui/toggle-group.tsx b/app-next/src/components/ui/toggle-group.tsx
new file mode 100644
index 00000000..27ec9aae
--- /dev/null
+++ b/app-next/src/components/ui/toggle-group.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import * as React from "react";
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
+import { type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+import { toggleVariants } from "@/components/ui/toggle";
+
+const ToggleGroupContext = React.createContext<
+ VariantProps & {
+ spacing?: number;
+ }
+>({
+ size: "default",
+ variant: "default",
+ spacing: 0,
+});
+
+function ToggleGroup({
+ className,
+ variant,
+ size,
+ spacing = 0,
+ children,
+ ...props
+}: React.ComponentProps &
+ VariantProps & {
+ spacing?: number;
+ }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function ToggleGroupItem({
+ className,
+ children,
+ variant,
+ size,
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ const context = React.useContext(ToggleGroupContext);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { ToggleGroup, ToggleGroupItem };
diff --git a/app-next/src/components/ui/toggle.tsx b/app-next/src/components/ui/toggle.tsx
new file mode 100644
index 00000000..47517bde
--- /dev/null
+++ b/app-next/src/components/ui/toggle.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import * as React from "react";
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const toggleVariants = cva(
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline:
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
+ },
+ size: {
+ default: "h-9 px-2 min-w-9",
+ sm: "h-8 px-1.5 min-w-8",
+ lg: "h-10 px-2.5 min-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function Toggle({
+ className,
+ variant,
+ size,
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ );
+}
+
+export { Toggle, toggleVariants };
diff --git a/app-next/src/components/ui/tooltip.tsx b/app-next/src/components/ui/tooltip.tsx
new file mode 100644
index 00000000..016298cc
--- /dev/null
+++ b/app-next/src/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils";
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/app-next/src/hooks/use-entity.ts b/app-next/src/hooks/use-entity.ts
new file mode 100644
index 00000000..398d1e97
--- /dev/null
+++ b/app-next/src/hooks/use-entity.ts
@@ -0,0 +1,70 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+import { apiClient } from "@/lib/api";
+import type { Dataset, Task, Flow, Run } from "@/types";
+
+/**
+ * React Query hook for fetching a dataset by ID
+ *
+ * @example
+ * const { data: dataset, isLoading, error } = useDataset(123);
+ */
+export function useDataset(
+ id: number | undefined,
+ options?: Omit, "queryKey" | "queryFn">,
+) {
+ return useQuery({
+ queryKey: ["dataset", id],
+ queryFn: () => apiClient.getDataset(id!),
+ enabled: !!id, // Only fetch if ID is provided
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ ...options,
+ });
+}
+
+/**
+ * React Query hook for fetching a task by ID
+ */
+export function useTask(
+ id: number | undefined,
+ options?: Omit, "queryKey" | "queryFn">,
+) {
+ return useQuery({
+ queryKey: ["task", id],
+ queryFn: () => apiClient.getTask(id!),
+ enabled: !!id,
+ staleTime: 5 * 60 * 1000,
+ ...options,
+ });
+}
+
+/**
+ * React Query hook for fetching a flow by ID
+ */
+export function useFlow(
+ id: number | undefined,
+ options?: Omit, "queryKey" | "queryFn">,
+) {
+ return useQuery({
+ queryKey: ["flow", id],
+ queryFn: () => apiClient.getFlow(id!),
+ enabled: !!id,
+ staleTime: 5 * 60 * 1000,
+ ...options,
+ });
+}
+
+/**
+ * React Query hook for fetching a run by ID
+ */
+export function useRun(
+ id: number | undefined,
+ options?: Omit, "queryKey" | "queryFn">,
+) {
+ return useQuery({
+ queryKey: ["run", id],
+ queryFn: () => apiClient.getRun(id!),
+ enabled: !!id,
+ staleTime: 5 * 60 * 1000,
+ ...options,
+ });
+}
diff --git a/app-next/src/lib/api.ts b/app-next/src/lib/api.ts
new file mode 100644
index 00000000..8eb88e67
--- /dev/null
+++ b/app-next/src/lib/api.ts
@@ -0,0 +1,172 @@
+import axios, { AxiosInstance, AxiosError } from "axios";
+import type {
+ Dataset,
+ Task,
+ Flow,
+ Run,
+ SearchableEntity,
+ EntityType,
+} from "@/types";
+
+/**
+ * OpenML API Client - Type-safe wrapper around Flask backend
+ *
+ * This client provides methods to interact with the Flask API backend,
+ * with proper TypeScript typing and error handling.
+ */
+className OpenMLAPIClient {
+ private client: AxiosInstance;
+
+ constructor() {
+ this.client = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000",
+ timeout: 10000,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ // Request interceptor (for adding auth tokens, logging, etc.)
+ this.client.interceptors.request.use(
+ (config) => {
+ // Add authentication token if available
+ const token =
+ typeof window !== "undefined"
+ ? localStorage.getItem("auth_token")
+ : null;
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ },
+ );
+
+ // Response interceptor (for error normalization)
+ this.client.interceptors.response.use(
+ (response) => response,
+ (error: AxiosError) => {
+ // Normalize error messages
+ if (error.response) {
+ // Server responded with error status
+ console.error(
+ "API Error:",
+ error.response.status,
+ error.response.data,
+ );
+ } else if (error.request) {
+ // No response received
+ console.error("Network Error:", error.message);
+ } else {
+ // Error setting up request
+ console.error("Request Error:", error.message);
+ }
+ return Promise.reject(error);
+ },
+ );
+ }
+
+ /**
+ * Fetch a single dataset by ID
+ */
+ async getDataset(id: number): Promise {
+ const { data } = await this.client.get(`/api/v1/data/${id}`);
+ return data;
+ }
+
+ /**
+ * Fetch a single task by ID
+ */
+ async getTask(id: number): Promise {
+ const { data } = await this.client.get(`/api/v1/task/${id}`);
+ return data;
+ }
+
+ /**
+ * Fetch a single flow by ID
+ */
+ async getFlow(id: number): Promise {
+ const { data } = await this.client.get(`/api/v1/flow/${id}`);
+ return data;
+ }
+
+ /**
+ * Fetch a single run by ID
+ */
+ async getRun(id: number): Promise {
+ const { data } = await this.client.get(`/api/v1/run/${id}`);
+ return data;
+ }
+
+ /**
+ * Generic search method
+ */
+ async search(
+ entityType: EntityType,
+ query: string,
+ filters?: Record,
+ page: number = 1,
+ pageSize: number = 20,
+ ): Promise<{ results: T[]; total: number }> {
+ const { data } = await this.client.post(`/api/search/${entityType}`, {
+ query,
+ filters,
+ page,
+ page_size: pageSize,
+ });
+ return data;
+ }
+
+ /**
+ * Search datasets
+ */
+ async searchDatasets(
+ query: string,
+ filters?: Record,
+ page: number = 1,
+ pageSize: number = 20,
+ ): Promise<{ results: Dataset[]; total: number }> {
+ return this.search("data", query, filters, page, pageSize);
+ }
+
+ /**
+ * Search tasks
+ */
+ async searchTasks(
+ query: string,
+ filters?: Record,
+ page: number = 1,
+ pageSize: number = 20,
+ ): Promise<{ results: Task[]; total: number }> {
+ return this.search("task", query, filters, page, pageSize);
+ }
+
+ /**
+ * Search flows
+ */
+ async searchFlows(
+ query: string,
+ filters?: Record,
+ page: number = 1,
+ pageSize: number = 20,
+ ): Promise<{ results: Flow[]; total: number }> {
+ return this.search("flow", query, filters, page, pageSize);
+ }
+
+ /**
+ * Search runs
+ */
+ async searchRuns(
+ query: string,
+ filters?: Record,
+ page: number = 1,
+ pageSize: number = 20,
+ ): Promise<{ results: Run[]; total: number }> {
+ return this.search("run", query, filters, page, pageSize);
+ }
+}
+
+// Export singleton instance
+export const apiClient = new OpenMLAPIClient();
diff --git a/app-next/src/lib/elasticsearch/configs/dataset-config.ts b/app-next/src/lib/elasticsearch/configs/dataset-config.ts
new file mode 100644
index 00000000..f2edb456
--- /dev/null
+++ b/app-next/src/lib/elasticsearch/configs/dataset-config.ts
@@ -0,0 +1,126 @@
+/**
+ * Dataset Search Configuration
+ *
+ * Ported from /app/src/search_configs/dataConfig.js
+ * Defines search behavior, fields, weights, facets, and aggregations
+ */
+
+import { OpenMLSearchConnector } from "../openml-search-connector";
+
+const apiConnector = new OpenMLSearchConnector("data");
+
+const datasetSearchConfig = {
+ apiConnector,
+ alwaysSearchOnInitialLoad: true,
+ searchQuery: {
+ resultsPerPage: 100,
+ search_fields: {
+ name: { weight: 3 },
+ exact_name: { weight: 3 },
+ description: { weight: 3 },
+ "tags.tag": { weight: 3 },
+ uploader: { weight: 2 },
+ format: { weight: 1 },
+ licence: { weight: 1 },
+ status: { weight: 1 },
+ error_message: { weight: 1 },
+ default_className_attribute: { weight: 1 },
+ },
+ result_fields: {
+ contributor: { raw: {} },
+ creator: { raw: {} },
+ data_id: { raw: {} },
+ date: { raw: {} },
+ name: {
+ snippet: {
+ size: 100,
+ fallback: true,
+ },
+ },
+ format: { raw: {} },
+ description: {
+ snippet: {
+ size: 100,
+ fallback: true,
+ },
+ },
+ "qualities.NumberOfInstances": { raw: {} },
+ "qualities.NumberOfFeatures": { raw: {} },
+ "qualities.NumberOfclassNamees": { raw: {} },
+ "qualities.NumberOfMissingValues": { raw: {} },
+ last_update: { raw: {} },
+ licence: { raw: {} },
+ nr_of_likes: { raw: {} },
+ nr_of_downloads: { raw: {} },
+ row_id_attribute: { raw: {} },
+ runs: { raw: {} },
+ status: { raw: {} },
+ tags: { raw: {} },
+ total_downloads: { raw: {} },
+ update_comment: { raw: {} },
+ uploader: { raw: {} },
+ uploader_id: { raw: {} },
+ url: { raw: {} },
+ version: { raw: {} },
+ version_label: { raw: {} },
+ visibility: { raw: {} },
+ suggest: { raw: {} },
+ },
+ disjunctiveFacets: [
+ "status",
+ "licence",
+ "qualities.NumberOfInstances",
+ "qualities.NumberOfFeatures",
+ "qualities.NumberOfclassNamees",
+ "format",
+ ],
+ facets: {
+ "status.keyword": { type: "value" },
+ "name.keyword": { type: "value" },
+ "licence.keyword": { type: "value" },
+ "qualities.NumberOfInstances": {
+ type: "range",
+ ranges: [
+ { from: 0, to: 999, name: "100s" },
+ { from: 1000, to: 9999, name: "1000s" },
+ { from: 10000, to: 99999, name: "10000s" },
+ { from: 100000, to: 999999, name: "100000s" },
+ { from: 1000000, name: "Millions" },
+ ],
+ },
+ "qualities.NumberOfFeatures": {
+ type: "range",
+ ranges: [
+ { from: 0, to: 10, name: "Less than 10" },
+ { from: 10, to: 100, name: "10s" },
+ { from: 100, to: 1000, name: "100s" },
+ { from: 1000, to: 10000, name: "1000s" },
+ { from: 10000, name: "10000s" },
+ ],
+ },
+ "qualities.NumberOfclassNamees": {
+ type: "range",
+ ranges: [
+ { from: 0, to: 2, name: "Regression" },
+ { from: 2, to: 2, name: "Binary classNameification" },
+ { from: 2, name: "Multi-className" },
+ ],
+ },
+ format: { type: "value" },
+ },
+ },
+ initialState: {
+ sortList: [{ field: "runs", direction: "desc" }],
+ },
+ autocompleteQuery: {
+ search_fields: {
+ name: {},
+ },
+ result_fields: {
+ name: { snippet: { size: 100, fallback: true } },
+ url: { raw: {} },
+ },
+ },
+};
+
+export default datasetSearchConfig;
diff --git a/app-next/src/lib/elasticsearch/get-dataset.ts b/app-next/src/lib/elasticsearch/get-dataset.ts
new file mode 100644
index 00000000..89e09175
--- /dev/null
+++ b/app-next/src/lib/elasticsearch/get-dataset.ts
@@ -0,0 +1,89 @@
+import type { Dataset } from "@/types/dataset";
+
+/**
+ * Server-side utility to fetch a dataset from Elasticsearch
+ *
+ * This function runs on the server and fetches dataset data directly from
+ * the Elasticsearch API. It's used in Server Components for optimal performance.
+ *
+ * @param id - The dataset ID to fetch
+ * @returns Promise - The dataset data
+ * @throws Error if dataset not found or API error
+ */
+export async function getDataset(id: number): Promise {
+ const ELASTICSEARCH_SERVER =
+ process.env.NEXT_PUBLIC_ELASTICSEARCH_SERVER ||
+ "https://www.openml.org/es/";
+
+ const url = `${ELASTICSEARCH_SERVER}data/data/${id}`;
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ // ISR: Revalidate every hour (3600 seconds)
+ // This provides a good balance between freshness and performance
+ next: { revalidate: 3600 },
+ });
+
+ if (!response.ok) {
+ console.error(
+ `Elasticsearch error: ${response.status} ${response.statusText}`,
+ );
+ throw new Error(`Failed to fetch dataset ${id}`);
+ }
+
+ const data = await response.json();
+
+ // Elasticsearch returns data in { found: boolean, _source: {...} } format
+ if (!data.found) {
+ throw new Error(`Dataset ${id} not found`);
+ }
+
+ return data._source as Dataset;
+ } catch (error) {
+ console.error(`Error fetching dataset ${id}:`, error);
+ throw error;
+ }
+}
+
+/**
+ * Get multiple datasets by IDs (batch fetch)
+ * Useful for related datasets or version history
+ */
+export async function getDatasets(ids: number[]): Promise {
+ const ELASTICSEARCH_SERVER =
+ process.env.NEXT_PUBLIC_ELASTICSEARCH_SERVER ||
+ "https://www.openml.org/es/";
+
+ const url = `${ELASTICSEARCH_SERVER}data/_mget`;
+
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ ids: ids.map(String),
+ }),
+ next: { revalidate: 3600 },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch datasets`);
+ }
+
+ const data = await response.json();
+
+ return data.docs
+ .filter((doc: any) => doc.found)
+ .map((doc: any) => doc._source as Dataset);
+ } catch (error) {
+ console.error("Error fetching multiple datasets:", error);
+ throw error;
+ }
+}
diff --git a/app-next/src/lib/elasticsearch/openml-search-connector.ts b/app-next/src/lib/elasticsearch/openml-search-connector.ts
new file mode 100644
index 00000000..07f90e30
--- /dev/null
+++ b/app-next/src/lib/elasticsearch/openml-search-connector.ts
@@ -0,0 +1,333 @@
+/**
+ * OpenML Elasticsearch Search Connector
+ *
+ * Custom connector for direct Elasticsearch queries without using
+ * the problematic @elastic/search-ui-elasticsearch-connector package.
+ *
+ * Ported from /app/src/services/OpenMLSearchConnector.js with TypeScript types.
+ */
+
+type EntityType = "data" | "task" | "flow" | "run";
+
+// Custom type definitions (these types aren't exported from @elastic/react-search-ui)
+interface SearchRequest {
+ searchTerm?: string;
+ filters?: Record;
+ current?: number;
+ resultsPerPage?: number;
+ sortList?: Array<{ field: string; direction: "asc" | "desc" }>;
+ sortField?: string;
+ sortDirection?: "asc" | "desc";
+}
+
+interface SearchResponse {
+ results: any[];
+ totalResults: number;
+ facets: Record;
+ requestId: string;
+}
+
+interface QueryConfig {
+ search_fields?: Record;
+ facets?: Record;
+ result_fields?: Record;
+}
+
+interface Filter {
+ type?: "any" | "all";
+ value?: any;
+ from?: number;
+ to?: number;
+}
+
+interface FacetValue {
+ type: "value";
+ data: Array<{ value: any; count: number }>;
+}
+
+interface ElasticsearchQuery {
+ query: {
+ bool: {
+ must: any[];
+ filter: any[];
+ };
+ };
+ from: number;
+ size: number;
+ aggs?: Record;
+ sort?: Array>;
+}
+
+interface ElasticsearchHit {
+ _id: string;
+ _score: number;
+ _source: Record;
+}
+
+interface ElasticsearchResponse {
+ hits: {
+ total: { value: number } | number;
+ hits: ElasticsearchHit[];
+ };
+ aggregations?: Record;
+}
+
+/**
+ * OpenML Search Connector className
+ */
+export class OpenMLSearchConnector {
+ private baseUrl: string;
+ private indexName: EntityType;
+
+ constructor(indexName: EntityType) {
+ this.baseUrl =
+ process.env.NEXT_PUBLIC_ELASTICSEARCH_SERVER ||
+ "https://www.openml.org/es/";
+ this.indexName = indexName;
+ }
+
+ /**
+ * Build Elasticsearch query from Search UI request state
+ */
+ private buildQuery(
+ requestState: SearchRequest,
+ queryConfig: QueryConfig,
+ ): ElasticsearchQuery {
+ const {
+ searchTerm,
+ filters,
+ current = 1,
+ resultsPerPage = 100,
+ sortList,
+ sortField,
+ sortDirection,
+ } = requestState;
+
+ const query: ElasticsearchQuery["query"] = {
+ bool: {
+ must: [],
+ filter: [],
+ },
+ };
+
+ // Add search term with weighted fields
+ if (searchTerm) {
+ const searchFields = queryConfig.search_fields || {};
+ const weightedFields = Object.entries(searchFields).map(
+ ([field, config]: [string, any]) => `${field}^${config.weight || 1}`,
+ );
+
+ query.bool.must.push({
+ multi_match: {
+ query: searchTerm,
+ fields: weightedFields,
+ },
+ });
+ } else {
+ // Match all if no search term
+ query.bool.must.push({ match_all: {} });
+ }
+
+ // Add filters
+ if (filters) {
+ Object.entries(filters).forEach(([fieldName, filterValues]) => {
+ (filterValues as Filter[]).forEach((filterValue) => {
+ if (filterValue.type === "any" || filterValue.type === "all") {
+ // Term filter for exact matches
+ query.bool.filter.push({
+ term: { [fieldName]: filterValue.value },
+ });
+ } else if ("from" in filterValue || "to" in filterValue) {
+ // Range filter for numeric fields
+ const range: any = {};
+ if ("from" in filterValue && filterValue.from !== undefined) {
+ range.gte = filterValue.from;
+ }
+ if ("to" in filterValue && filterValue.to !== undefined) {
+ range.lte = filterValue.to;
+ }
+ query.bool.filter.push({
+ range: { [fieldName]: range },
+ });
+ }
+ });
+ });
+ }
+
+ // Build aggregations for facets
+ const aggs: Record = {};
+ if (queryConfig.facets) {
+ Object.entries(queryConfig.facets).forEach(
+ ([fieldName, facetConfig]: [string, any]) => {
+ if (facetConfig.type === "value") {
+ aggs[fieldName] = {
+ terms: {
+ field: fieldName,
+ size: facetConfig.size || 10,
+ },
+ };
+ } else if (facetConfig.type === "range") {
+ // Strip 'name' field from ranges - ES doesn't accept it
+ const esRanges = facetConfig.ranges.map((range: any) => {
+ const { name, ...esRange } = range;
+ return esRange;
+ });
+ aggs[fieldName] = {
+ range: {
+ field: fieldName,
+ ranges: esRanges,
+ },
+ };
+ }
+ },
+ );
+ }
+
+ const from = (current - 1) * resultsPerPage;
+ const size = resultsPerPage;
+
+ const esQuery: ElasticsearchQuery = {
+ query,
+ from,
+ size,
+ aggs: Object.keys(aggs).length > 0 ? aggs : undefined,
+ };
+
+ // Add sorting - Search UI uses sortList array format
+ if (sortList && sortList.length > 0) {
+ esQuery.sort = sortList.map((sortItem: any) => ({
+ [sortItem.field]: { order: sortItem.direction },
+ }));
+ } else if (sortField && sortDirection) {
+ // Fallback for direct sortField/sortDirection
+ esQuery.sort = [
+ { [sortField]: { order: sortDirection as "asc" | "desc" } },
+ ];
+ }
+
+ return esQuery;
+ }
+
+ /**
+ * Format Elasticsearch response to Search UI format
+ * Search UI expects fields in { raw: value } format
+ */
+ private formatResponse(
+ esResponse: ElasticsearchResponse,
+ requestState: SearchRequest,
+ ): SearchResponse {
+ const { hits, aggregations } = esResponse;
+
+ const results = hits.hits.map((hit) => {
+ const formattedResult: any = {
+ id: { raw: hit._id },
+ _meta: {
+ id: hit._id,
+ score: hit._score,
+ rawHit: {
+ _type: this.indexName,
+ },
+ },
+ };
+
+ // Wrap all source fields in { raw: value } format
+ Object.entries(hit._source).forEach(([key, value]) => {
+ formattedResult[key] = { raw: value };
+ });
+
+ return formattedResult;
+ });
+
+ const totalResults =
+ typeof hits.total === "number" ? hits.total : hits.total.value;
+
+ const facets: Record = {};
+ if (aggregations) {
+ Object.entries(aggregations).forEach(
+ ([fieldName, agg]: [string, any]) => {
+ if (agg.buckets) {
+ facets[fieldName] = [
+ {
+ type: "value" as const,
+ data: agg.buckets.map((bucket: any) => ({
+ value: bucket.key,
+ count: bucket.doc_count,
+ })),
+ },
+ ];
+ }
+ },
+ );
+ }
+
+ return {
+ results,
+ totalResults,
+ facets,
+ requestId: Date.now().toString(),
+ };
+ }
+
+ /**
+ * Main search method called by Search UI
+ */
+ async onSearch(
+ requestState: SearchRequest,
+ queryConfig: QueryConfig,
+ ): Promise {
+ try {
+ const esQuery = this.buildQuery(requestState, queryConfig);
+
+ const url = `${this.baseUrl}${this.indexName}/_search`;
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(esQuery),
+ });
+
+ const data: ElasticsearchResponse = await response.json();
+
+ if (!response.ok) {
+ console.error("[OpenMLSearchConnector] ES Error:", data);
+ throw new Error(
+ `Elasticsearch error: ${response.status} ${response.statusText}`,
+ );
+ }
+
+ return this.formatResponse(data, requestState);
+ } catch (error) {
+ console.error("[OpenMLSearchConnector] Search error:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Autocomplete method (optional)
+ */
+ async onAutocomplete(
+ requestState: SearchRequest,
+ queryConfig: QueryConfig,
+ ): Promise<{ results: any[] }> {
+ // Return empty results for now - can be enhanced later
+ return { results: [] };
+ }
+
+ /**
+ * Result click tracking (optional)
+ */
+ onResultClick(): void {
+ // No-op for now
+ }
+
+ /**
+ * Autocomplete result click tracking (optional)
+ */
+ onAutocompleteResultClick(): void {
+ // No-op for now
+ }
+}
+
+export default OpenMLSearchConnector;
diff --git a/app-next/src/lib/utils.ts b/app-next/src/lib/utils.ts
new file mode 100644
index 00000000..ea8ac1b8
--- /dev/null
+++ b/app-next/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type classNameValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: classNameValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/app-next/src/proxy.ts b/app-next/src/proxy.ts
new file mode 100644
index 00000000..bf9384a6
--- /dev/null
+++ b/app-next/src/proxy.ts
@@ -0,0 +1,73 @@
+import { NextResponse } from "next/server";
+import type { NextRequest } from "next/server";
+
+/**
+ * Proxy for backward-compatible URL redirects
+ *
+ * Academic papers and external links often cite OpenML entities using short URLs:
+ * - /d/:id (datasets)
+ * - /t/:id (tasks)
+ * - /f/:id (flows)
+ * - /r/:id (runs)
+ *
+ * This proxy ensures these legacy URLs redirect to the new SEO-friendly URLs:
+ * - /d/:id β /datasets/:id (301 permanent redirect)
+ * - /t/:id β /tasks/:id (301 permanent redirect)
+ * - /f/:id β /flows/:id (301 permanent redirect)
+ * - /r/:id β /runs/:id (301 permanent redirect)
+ *
+ * This preserves:
+ * β
Academic citations
+ * β
Bookmarks
+ * β
Stack Overflow answers
+ * β
External blog posts
+ * β
Google PageRank (301 redirects pass link equity)
+ */
+export function proxy(request: NextRequest) {
+ const { pathname } = request.nextUrl;
+
+ // Map of legacy URL patterns to new SEO-friendly URLs
+ const redirects: Record = {
+ // Entity detail pages
+ "^/d/(\\d+)": "/datasets/$1",
+ "^/t/(\\d+)": "/tasks/$1",
+ "^/f/(\\d+)": "/flows/$1",
+ "^/r/(\\d+)": "/runs/$1",
+
+ // Search pages (backward compatibility)
+ "^/d/search": "/datasets",
+ "^/t/search": "/tasks",
+ "^/f/search": "/flows",
+ "^/r/search": "/runs",
+ };
+
+ // Check each redirect pattern
+ for (const [pattern, replacement] of Object.entries(redirects)) {
+ const regex = new RegExp(pattern);
+ const match = pathname.match(regex);
+
+ if (match) {
+ // Construct the new URL with the entity ID (if present)
+ const newPath = pathname.replace(regex, replacement);
+ const url = request.nextUrl.clone();
+ url.pathname = newPath;
+
+ // Preserve query parameters (important for search filters/sorting)
+ // e.g., /d/search?status=active β /datasets?status=active
+
+ // Return 301 (Permanent Redirect) to preserve SEO
+ return NextResponse.redirect(url, { status: 301 });
+ }
+ }
+
+ // No redirect needed, continue to the requested page
+ return NextResponse.next();
+}
+
+/**
+ * Configure which routes this proxy should run on
+ * We only want to check legacy short URL patterns
+ */
+export const config = {
+ matcher: ["/d/:path*", "/t/:path*", "/f/:path*", "/r/:path*"],
+};
diff --git a/app-next/src/services/OpenMLSearchConnector.ts b/app-next/src/services/OpenMLSearchConnector.ts
new file mode 100644
index 00000000..52a582f4
--- /dev/null
+++ b/app-next/src/services/OpenMLSearchConnector.ts
@@ -0,0 +1,302 @@
+import {
+ QueryConfig,
+ RequestState,
+ ResponseState,
+ AutocompleteResponseState,
+ APIConnector,
+} from "@elastic/search-ui";
+
+interface FilterValue {
+ type: "any" | "all";
+ value: string;
+ from?: number;
+ to?: number;
+ name?: string;
+}
+
+interface FacetConfig {
+ type: "value" | "range";
+ size?: number;
+ ranges?: { from?: number; to?: number; name: string }[];
+}
+
+class OpenMLSearchConnector implements APIConnector {
+ baseUrl: string;
+ indexName: string;
+
+ constructor(indexName: string) {
+ this.baseUrl = "https://www.openml.org/es/";
+ this.indexName = indexName;
+ }
+
+ onResultClick(): void {
+ // No-op
+ }
+
+ onAutocompleteResultClick(): void {
+ // No-op
+ }
+
+ /**
+ * Build Elasticsearch query from Search UI request state
+ */
+ buildQuery(requestState: RequestState, queryConfig: QueryConfig) {
+ const {
+ searchTerm,
+ filters,
+ current,
+ resultsPerPage,
+ sortField,
+ sortDirection,
+ sortList,
+ } = requestState;
+
+ const query: any = {
+ bool: {
+ must: [],
+ filter: [],
+ },
+ };
+
+ // Add search term if provided
+ if (searchTerm) {
+ query.bool.must.push({
+ multi_match: {
+ query: searchTerm,
+ fields: Object.keys(queryConfig.search_fields || {}).map(
+ (field) =>
+ `${field}^${queryConfig.search_fields?.[field]?.weight || 1}`,
+ ),
+ },
+ });
+ } else {
+ // Match all if no search term
+ query.bool.must.push({ match_all: {} });
+ }
+
+ // Add filters
+ if (filters) {
+ filters.forEach((filter) => {
+ filter.values.forEach((filterValue: any) => {
+ if (filter.type === "any") {
+ // Term filter for exact matches
+ query.bool.filter.push({
+ term: { [filter.field]: filterValue },
+ });
+ } else if (filter.type === "all") {
+ // Must match all values
+ query.bool.filter.push({
+ term: { [filter.field]: filterValue },
+ });
+ } else if (filterValue.from || filterValue.to) {
+ // Range filter for numeric fields
+ const range: any = {};
+ if (filterValue.from !== undefined) range.gte = filterValue.from;
+ if (filterValue.to !== undefined) range.lte = filterValue.to;
+ query.bool.filter.push({
+ range: { [filter.field]: range },
+ });
+ }
+ });
+ });
+ }
+
+ // Build aggregations for facets
+ const aggs: any = {};
+ if (queryConfig.facets) {
+ Object.entries(queryConfig.facets).forEach(
+ ([fieldName, facetConfig]: [string, any]) => {
+ if (facetConfig.type === "value") {
+ aggs[fieldName] = {
+ terms: {
+ field: fieldName,
+ size: facetConfig.size || 10,
+ },
+ };
+ } else if (facetConfig.type === "range") {
+ // Strip out 'name' field from ranges - ES doesn't accept it
+ const esRanges = facetConfig.ranges.map((range: any) => {
+ const { name, ...esRange } = range;
+ return esRange;
+ });
+ aggs[fieldName] = {
+ range: {
+ field: fieldName,
+ ranges: esRanges,
+ },
+ };
+ }
+ },
+ );
+ }
+
+ // Sanitize resultsPerPage (handle legacy formats like "n_20_n")
+ let size = 10;
+ if (resultsPerPage) {
+ if (typeof resultsPerPage === "number") {
+ size = resultsPerPage;
+ } else if (typeof resultsPerPage === "string") {
+ // Try to extract number from string (e.g. "n_20_n" -> 20)
+ const match = (resultsPerPage as string).match(/\d+/);
+ if (match) {
+ size = parseInt(match[0], 10);
+ }
+ }
+ }
+
+ // Ensure valid pagination
+ const validCurrent = current && current > 0 ? current : 1;
+ const from = (validCurrent - 1) * size;
+
+ const esQuery: any = {
+ query,
+ from,
+ size,
+ aggs,
+ };
+
+ // Add sorting - Search UI uses sortList array format
+ if (sortList && sortList.length > 0) {
+ esQuery.sort = sortList.map((sortItem) => ({
+ [sortItem.field]: { order: sortItem.direction },
+ }));
+ } else if (sortField && sortDirection) {
+ // Fallback for direct sortField/sortDirection
+ esQuery.sort = [{ [sortField]: { order: sortDirection } }];
+ }
+
+ return esQuery;
+ }
+
+ /**
+ * Format Elasticsearch response to Search UI format
+ * Search UI expects fields in { raw: value } format
+ */
+ formatResponse(esResponse: any, requestState: RequestState): ResponseState {
+ const { hits, aggregations } = esResponse;
+
+ const results = hits.hits.map((hit: any) => {
+ const formattedResult: any = {
+ id: { raw: hit._id },
+ _meta: {
+ id: hit._id,
+ score: hit._score,
+ rawHit: {
+ _type: this.indexName, // Add the index name as the type
+ },
+ },
+ };
+
+ // Wrap all source fields in { raw: value } format
+ Object.entries(hit._source).forEach(([key, value]) => {
+ formattedResult[key] = { raw: value };
+ });
+
+ return formattedResult;
+ });
+
+ const totalResults = hits.total.value || hits.total;
+
+ const facets: any = {};
+ if (aggregations) {
+ Object.entries(aggregations).forEach(
+ ([fieldName, agg]: [string, any]) => {
+ if (agg.buckets) {
+ facets[fieldName] = [
+ {
+ type: "value",
+ data: agg.buckets.map((bucket: any) => ({
+ value: bucket.key,
+ count: bucket.doc_count,
+ })),
+ },
+ ];
+ }
+ },
+ );
+ }
+
+ return {
+ results,
+ totalResults,
+ facets,
+ requestId: Date.now().toString(),
+ totalPages: Math.ceil(totalResults / (requestState.resultsPerPage || 10)),
+ pagingStart:
+ ((requestState.current || 1) - 1) *
+ (requestState.resultsPerPage || 10) +
+ 1,
+ pagingEnd: Math.min(
+ (requestState.current || 1) * (requestState.resultsPerPage || 10),
+ totalResults,
+ ),
+ wasSearched: true,
+ resultSearchTerm: requestState.searchTerm || "",
+ rawResponse: esResponse,
+ };
+ }
+
+ /**
+ * Main search method called by Search UI
+ */
+ async onSearch(
+ requestState: RequestState,
+ queryConfig: QueryConfig,
+ ): Promise {
+ console.log("[OpenMLSearchConnector] onSearch Request:", requestState);
+ try {
+ const esQuery = this.buildQuery(requestState, queryConfig);
+ console.log(
+ "[OpenMLSearchConnector] Built ES Query:",
+ JSON.stringify(esQuery, null, 2),
+ );
+
+ const url = `${this.baseUrl}${this.indexName}/_search`;
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(esQuery),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(
+ `ES responded with ${response.status}: ${response.statusText}. Error: ${JSON.stringify(
+ data,
+ )}`,
+ );
+ }
+
+ console.log(
+ "[OpenMLSearchConnector] ES Response Hits:",
+ data.hits?.total,
+ );
+
+ return this.formatResponse(data, requestState);
+ } catch (error) {
+ console.error("[OpenMLSearchConnector] Error:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Autocomplete method (optional, called by Search UI for suggestions)
+ */
+ async onAutocomplete(
+ requestState: RequestState,
+ queryConfig: any, // Relaxed type to match base className expectation or ignore mismatch
+ ): Promise {
+ return {
+ autocompletedResults: [],
+ autocompletedSuggestions: {},
+ autocompletedResultsRequestId: "",
+ autocompletedSuggestionsRequestId: "",
+ };
+ }
+}
+
+export default OpenMLSearchConnector;
diff --git a/app-next/src/types/dataset.ts b/app-next/src/types/dataset.ts
new file mode 100644
index 00000000..e86ce5c1
--- /dev/null
+++ b/app-next/src/types/dataset.ts
@@ -0,0 +1,113 @@
+/**
+ * Feature information for a dataset
+ */
+export interface DatasetFeature {
+ index: number;
+ name: string;
+ type: "nominal" | "numeric" | "string" | "date";
+ distinct?: number;
+ missing?: number;
+ target?: string; // "1" if target, undefined otherwise
+ min?: string;
+ max?: string;
+ mean?: string;
+ stdev?: string;
+ distr?: any[]; // Distribution data for charts
+}
+
+/**
+ * Tag information
+ */
+export interface DatasetTag {
+ tag: string;
+ uploader?: number;
+ window?: number;
+}
+
+/**
+ * OpenML Dataset Entity
+ * Represents a dataset in the OpenML platform
+ */
+export interface Dataset {
+ // Core identifiers
+ data_id: number;
+ name: string;
+ version: number;
+ version_label?: string;
+
+ // Metadata
+ description: string;
+ format: string;
+
+ // Uploader information
+ uploader: string; // uploader name
+ uploader_id: number;
+
+ // Dates
+ date: string; // upload date
+ upload_date?: string;
+ processing_date?: string;
+
+ // Status and visibility
+ status: "active" | "deactivated" | "in_preparation";
+ visibility?: "public" | "private";
+
+ // License
+ licence: string;
+
+ // Target and ID attributes
+ row_id_attribute?: string;
+ default_target_attribute?: string;
+ ignore_attribute?: string;
+
+ // Tags and categorization
+ tags: DatasetTag[];
+ tag?: string[]; // Alternative format
+
+ // Dataset statistics (summary)
+ instances?: number;
+ missing_values?: number;
+
+ // Features - detailed feature information
+ features: DatasetFeature[];
+
+ // Quality metrics (meta-features)
+ qualities: Record;
+
+ // File information
+ file_id?: number;
+ md5_checksum?: string;
+ url: string;
+
+ // Social metrics
+ nr_of_likes: number;
+ nr_of_downloads: number;
+ nr_of_issues: number;
+ runs?: number; // number of runs
+
+ // Related entities
+ tasks?: number[];
+
+ // Processing metadata
+ original_data_url?: string;
+ paper_url?: string;
+ citation?: string;
+ collection_date?: string;
+ language?: string;
+ creator?: string;
+}
+
+/**
+ * Dataset search result (includes Elasticsearch metadata)
+ */
+export interface DatasetSearchResult extends Dataset {
+ _meta?: {
+ score: number;
+ rawHit: {
+ _index: string;
+ _type: string;
+ _id: string;
+ _score: number;
+ };
+ };
+}
diff --git a/app-next/src/types/elasticsearch.ts b/app-next/src/types/elasticsearch.ts
new file mode 100644
index 00000000..0de6e431
--- /dev/null
+++ b/app-next/src/types/elasticsearch.ts
@@ -0,0 +1,83 @@
+/**
+ * Elasticsearch response types for OpenML search
+ */
+
+/**
+ * Generic Elasticsearch hit structure
+ */
+export interface ElasticsearchHit {
+ _index: string;
+ _type: string;
+ _id: string;
+ _score: number;
+ _source: T;
+}
+
+/**
+ * Elasticsearch response structure
+ */
+export interface ElasticsearchResponse {
+ took: number;
+ timed_out: boolean;
+ _shards: {
+ total: number;
+ successful: number;
+ skipped: number;
+ failed: number;
+ };
+ hits: {
+ total: {
+ value: number;
+ relation: "eq" | "gte";
+ };
+ max_score: number;
+ hits: ElasticsearchHit[];
+ };
+ aggregations?: Record;
+}
+
+/**
+ * Elasticsearch query parameters
+ */
+export interface ElasticsearchQuery {
+ index: string;
+ body: {
+ query: any;
+ size?: number;
+ from?: number;
+ sort?: any[];
+ aggs?: Record;
+ };
+}
+
+/**
+ * Search facet (filter) structure
+ */
+export interface SearchFacet {
+ field: string;
+ label: string;
+ type: "value" | "range";
+ values?: Array<{
+ value: string;
+ count: number;
+ }>;
+}
+
+/**
+ * Search filter state
+ */
+export interface SearchFilter {
+ field: string;
+ values: string[];
+ type: "match" | "term" | "range";
+}
+
+/**
+ * Pagination state
+ */
+export interface PaginationState {
+ current: number;
+ resultsPerPage: number;
+ totalResults: number;
+ totalPages: number;
+}
diff --git a/app-next/src/types/flow.ts b/app-next/src/types/flow.ts
new file mode 100644
index 00000000..bd017973
--- /dev/null
+++ b/app-next/src/types/flow.ts
@@ -0,0 +1,66 @@
+/**
+ * OpenML Flow Entity
+ * Represents a machine learning workflow/pipeline in the OpenML platform
+ */
+export interface Flow {
+ // Core identifiers
+ flow_id: number;
+ full_name?: string;
+ name: string;
+ custom_name?: string;
+ className_name?: string;
+ version: string;
+ external_version?: string;
+
+ // Metadata
+ description?: string;
+ uploader: number;
+ uploader_name?: string;
+ upload_date?: string;
+
+ // Dependencies
+ dependencies?: string;
+
+ // Parameters
+ parameter?: Array<{
+ name: string;
+ data_type: string;
+ default_value: string;
+ description?: string;
+ }>;
+
+ // Components (for complex flows)
+ components?: {
+ component: Array<{
+ identifier: string;
+ flow: Flow; // Recursive definition for nested flows
+ }>;
+ };
+
+ // Statistics
+ runs?: number;
+
+ // Tags
+ tag?: string[];
+
+ // Binary information
+ binary?: {
+ format: string;
+ md5_checksum: string;
+ };
+}
+
+/**
+ * Flow search result (includes Elasticsearch metadata)
+ */
+export interface FlowSearchResult extends Flow {
+ _meta?: {
+ score: number;
+ rawHit: {
+ _index: string;
+ _type: string;
+ _id: string;
+ _score: number;
+ };
+ };
+}
diff --git a/app-next/src/types/index.ts b/app-next/src/types/index.ts
new file mode 100644
index 00000000..15533ab4
--- /dev/null
+++ b/app-next/src/types/index.ts
@@ -0,0 +1,51 @@
+/**
+ * Central export file for all TypeScript types and interfaces
+ */
+
+import type { Dataset, DatasetSearchResult } from "./dataset";
+import type { Task, TaskSearchResult } from "./task";
+import type { Flow, FlowSearchResult } from "./flow";
+import type { Run, RunSearchResult } from "./run";
+
+// Re-export entity types
+export type { Dataset, DatasetSearchResult } from "./dataset";
+export type { Task, TaskSearchResult } from "./task";
+export type { Flow, FlowSearchResult } from "./flow";
+export type { Run, RunSearchResult } from "./run";
+
+// Elasticsearch types
+export type {
+ ElasticsearchHit,
+ ElasticsearchResponse,
+ ElasticsearchQuery,
+ SearchFacet,
+ SearchFilter,
+ PaginationState,
+} from "./elasticsearch";
+
+// Union type for all searchable entities
+export type SearchableEntity = Dataset | Task | Flow | Run;
+export type SearchableEntityResult =
+ | DatasetSearchResult
+ | TaskSearchResult
+ | FlowSearchResult
+ | RunSearchResult;
+
+// Entity type discriminator
+export type EntityType = "data" | "task" | "flow" | "run";
+
+// Entity ID field mapping
+export type EntityIdField = {
+ data: "data_id";
+ task: "task_id";
+ flow: "flow_id";
+ run: "run_id";
+};
+
+// URL path mapping
+export type EntityUrlPath = {
+ data: "/datasets";
+ task: "/tasks";
+ flow: "/flows";
+ run: "/runs";
+};
diff --git a/app-next/src/types/run.ts b/app-next/src/types/run.ts
new file mode 100644
index 00000000..bee072fc
--- /dev/null
+++ b/app-next/src/types/run.ts
@@ -0,0 +1,81 @@
+/**
+ * OpenML Run Entity
+ * Represents an experiment run in the OpenML platform
+ */
+export interface Run {
+ // Core identifiers
+ run_id: number;
+ uploader: number;
+ uploader_name?: string;
+
+ // Related entities
+ task_id: number;
+ task?: {
+ task_id: number;
+ task_type: string;
+ source_data: {
+ data_id: number;
+ name: string;
+ };
+ };
+
+ flow_id: number;
+ flow?: {
+ flow_id: number;
+ name: string;
+ };
+ flow_name?: string; // For backward compatibility
+
+ // Metadata
+ upload_time?: string;
+ error_message?: string | null;
+
+ // Parameter settings
+ parameter_setting?: Array<{
+ name: string;
+ value: string;
+ component?: {
+ identifier: string;
+ flow_id: number;
+ };
+ }>;
+
+ // Evaluation results
+ evaluations?: Array<{
+ measure: string;
+ value: number;
+ stdev?: number;
+ array_data?: string;
+ }>;
+
+ // Output files
+ output_files?: {
+ file: Array<{
+ name: string;
+ file_id: number;
+ url: string;
+ }>;
+ };
+
+ // Tags
+ tag?: string[];
+
+ // Setup information
+ setup_id?: number;
+ setup_string?: string;
+}
+
+/**
+ * Run search result (includes Elasticsearch metadata)
+ */
+export interface RunSearchResult extends Run {
+ _meta?: {
+ score: number;
+ rawHit: {
+ _index: string;
+ _type: string;
+ _id: string;
+ _score: number;
+ };
+ };
+}
diff --git a/app-next/src/types/task.ts b/app-next/src/types/task.ts
new file mode 100644
index 00000000..a4e6bf5e
--- /dev/null
+++ b/app-next/src/types/task.ts
@@ -0,0 +1,78 @@
+/**
+ * OpenML Task Entity
+ * Represents a machine learning task in the OpenML platform
+ */
+export interface Task {
+ // Core identifiers
+ task_id: number;
+ task_type_id: number;
+ task_type: string;
+
+ // Metadata
+ name?: string;
+ source_data: {
+ data_id: number;
+ name: string;
+ };
+ source_data_name?: string; // For backward compatibility
+
+ // Task configuration
+ target_feature?: string;
+ estimation_procedure?: {
+ type: string;
+ parameters: Record;
+ };
+ evaluation_measures?: string[];
+ cost_matrix?: number[][];
+
+ // Task type details
+ tasktype?: {
+ name: string;
+ description?: string;
+ };
+
+ // Statistics
+ runs?: number;
+
+ // Related entities
+ input?: {
+ source_data: {
+ data_set: {
+ data_set_id: number;
+ target_feature: string;
+ };
+ };
+ estimation_procedure: {
+ type: string;
+ data_splits_url: string;
+ parameters: Record;
+ };
+ evaluation_measures: {
+ evaluation_measure: string[];
+ };
+ };
+
+ // Tags
+ tag?: string[];
+
+ // Quality metrics
+ quality?: Record;
+
+ // Dates
+ upload_date?: string;
+}
+
+/**
+ * Task search result (includes Elasticsearch metadata)
+ */
+export interface TaskSearchResult extends Task {
+ _meta?: {
+ score: number;
+ rawHit: {
+ _index: string;
+ _type: string;
+ _id: string;
+ _score: number;
+ };
+ };
+}
diff --git a/app-next/try-outs_extraInfo/globals_extra.css b/app-next/try-outs_extraInfo/globals_extra.css
new file mode 100644
index 00000000..435115ce
--- /dev/null
+++ b/app-next/try-outs_extraInfo/globals_extra.css
@@ -0,0 +1,165 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.625rem; /* Slightly more refined rounding */
+
+ /* Light Mode - Clean, professional academic palette */
+ --background: oklch(0.99 0.005 250); /* Crisp white with subtle blue tint */
+ --foreground: oklch(0.15 0.02 250); /* Deep navy for excellent readability */
+
+ --card: oklch(1 0 0); /* Pure white cards */
+ --card-foreground: oklch(0.15 0.02 250);
+
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.15 0.02 250);
+
+ /* Primary - Deep navy blue, professional and trustworthy */
+ --primary: oklch(0.28 0.05 250); /* Sophisticated navy */
+ --primary-foreground: oklch(0.99 0 0); /* White */
+
+ /* Secondary - Warm gray for academic sophistication */
+ --secondary: oklch(0.96 0.005 250); /* Light gray with blue undertone */
+ --secondary-foreground: oklch(0.28 0.05 250);
+
+ /* Muted - Professional gray scale */
+ --muted: oklch(0.94 0.01 250); /* Very subtle blue tint */
+ --muted-foreground: oklch(0.45 0.02 250); /* Medium gray for secondary text */
+
+ /* Accent - Teal/blue-green for scientific appeal */
+ --accent: oklch(0.92 0.03 190); /* Fresh, clean teal accent */
+ --accent-foreground: oklch(0.28 0.05 250);
+
+ /* Destructive - Professional red for errors/warnings */
+ --destructive: oklch(0.58 0.2 25); /* Muted but clear red */
+
+ /* Borders - Subtle and refined */
+ --border: oklch(0.88 0.015 250); /* Very light blue-gray */
+ --input: oklch(0.95 0.01 250);
+ --ring: oklch(0.28 0.05 250); /* Match primary */
+
+ /* Charts - Scientific color palette (colorblind friendly) */
+ --chart-1: oklch(0.55 0.12 250); /* Navy */
+ --chart-2: oklch(0.6 0.15 190); /* Teal */
+ --chart-3: oklch(0.55 0.18 320); /* Purple */
+ --chart-4: oklch(0.65 0.16 80); /* Olive green */
+ --chart-5: oklch(0.58 0.14 30); /* Rust/orange */
+
+ /* Sidebar - Clean and distinct */
+ --sidebar: oklch(0.98 0.008 250); /* Slightly tinted white */
+ --sidebar-foreground: oklch(0.15 0.02 250);
+ --sidebar-primary: oklch(0.28 0.05 250);
+ --sidebar-primary-foreground: oklch(0.99 0 0);
+ --sidebar-accent: oklch(0.94 0.01 250);
+ --sidebar-accent-foreground: oklch(0.28 0.05 250);
+ --sidebar-border: oklch(0.88 0.015 250);
+ --sidebar-ring: oklch(0.55 0.08 250);
+}
+
+.dark {
+ /* Dark Mode - Professional dark theme for research */
+ --background: oklch(0.12 0.02 250); /* Deep navy-black */
+ --foreground: oklch(0.98 0.005 250); /* Soft white */
+
+ /* Cards with subtle depth */
+ --card: oklch(0.16 0.03 250); /* Slightly lighter than background */
+ --card-foreground: oklch(0.98 0.005 250);
+
+ --popover: oklch(0.16 0.03 250);
+ --popover-foreground: oklch(0.98 0.005 250);
+
+ /* Primary - Bright but professional blue */
+ --primary: oklch(0.75 0.12 250); /* Clear, readable blue */
+ --primary-foreground: oklch(0.12 0.02 250); /* Dark background */
+
+ /* Secondary - Medium dark gray */
+ --secondary: oklch(0.2 0.03 250);
+ --secondary-foreground: oklch(0.98 0.005 250);
+
+ /* Muted - Professional dark grays */
+ --muted: oklch(0.2 0.03 250);
+ --muted-foreground: oklch(0.65 0.03 250); /* Medium gray text */
+
+ /* Accent - Teal that works well in dark mode */
+ --accent: oklch(0.3 0.08 190);
+ --accent-foreground: oklch(0.98 0.005 250);
+
+ /* Destructive - Clear but not overwhelming */
+ --destructive: oklch(0.65 0.18 25);
+
+ /* Borders - Defined but subtle */
+ --border: oklch(0.25 0.04 250);
+ --input: oklch(0.2 0.03 250);
+ --ring: oklch(0.75 0.12 250); /* Match primary */
+
+ /* Charts - Optimized for dark background */
+ --chart-1: oklch(0.7 0.15 250); /* Bright navy */
+ --chart-2: oklch(0.75 0.18 190); /* Bright teal */
+ --chart-3: oklch(0.72 0.2 320); /* Bright purple */
+ --chart-4: oklch(0.78 0.16 80); /* Bright olive */
+ --chart-5: oklch(0.68 0.14 30); /* Bright rust */
+
+ /* Sidebar */
+ --sidebar: oklch(0.14 0.025 250);
+ --sidebar-foreground: oklch(0.98 0.005 250);
+ --sidebar-primary: oklch(0.75 0.12 250);
+ --sidebar-primary-foreground: oklch(0.12 0.02 250);
+ --sidebar-accent: oklch(0.2 0.03 250);
+ --sidebar-accent-foreground: oklch(0.98 0.005 250);
+ --sidebar-border: oklch(0.25 0.04 250);
+ --sidebar-ring: oklch(0.65 0.08 250);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ font-feature-settings:
+ "rlig" 1,
+ "calt" 1;
+ }
+}
diff --git a/app-next/try-outs_extraInfo/globals_original.css b/app-next/try-outs_extraInfo/globals_original.css
new file mode 100644
index 00000000..435115ce
--- /dev/null
+++ b/app-next/try-outs_extraInfo/globals_original.css
@@ -0,0 +1,165 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.625rem; /* Slightly more refined rounding */
+
+ /* Light Mode - Clean, professional academic palette */
+ --background: oklch(0.99 0.005 250); /* Crisp white with subtle blue tint */
+ --foreground: oklch(0.15 0.02 250); /* Deep navy for excellent readability */
+
+ --card: oklch(1 0 0); /* Pure white cards */
+ --card-foreground: oklch(0.15 0.02 250);
+
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.15 0.02 250);
+
+ /* Primary - Deep navy blue, professional and trustworthy */
+ --primary: oklch(0.28 0.05 250); /* Sophisticated navy */
+ --primary-foreground: oklch(0.99 0 0); /* White */
+
+ /* Secondary - Warm gray for academic sophistication */
+ --secondary: oklch(0.96 0.005 250); /* Light gray with blue undertone */
+ --secondary-foreground: oklch(0.28 0.05 250);
+
+ /* Muted - Professional gray scale */
+ --muted: oklch(0.94 0.01 250); /* Very subtle blue tint */
+ --muted-foreground: oklch(0.45 0.02 250); /* Medium gray for secondary text */
+
+ /* Accent - Teal/blue-green for scientific appeal */
+ --accent: oklch(0.92 0.03 190); /* Fresh, clean teal accent */
+ --accent-foreground: oklch(0.28 0.05 250);
+
+ /* Destructive - Professional red for errors/warnings */
+ --destructive: oklch(0.58 0.2 25); /* Muted but clear red */
+
+ /* Borders - Subtle and refined */
+ --border: oklch(0.88 0.015 250); /* Very light blue-gray */
+ --input: oklch(0.95 0.01 250);
+ --ring: oklch(0.28 0.05 250); /* Match primary */
+
+ /* Charts - Scientific color palette (colorblind friendly) */
+ --chart-1: oklch(0.55 0.12 250); /* Navy */
+ --chart-2: oklch(0.6 0.15 190); /* Teal */
+ --chart-3: oklch(0.55 0.18 320); /* Purple */
+ --chart-4: oklch(0.65 0.16 80); /* Olive green */
+ --chart-5: oklch(0.58 0.14 30); /* Rust/orange */
+
+ /* Sidebar - Clean and distinct */
+ --sidebar: oklch(0.98 0.008 250); /* Slightly tinted white */
+ --sidebar-foreground: oklch(0.15 0.02 250);
+ --sidebar-primary: oklch(0.28 0.05 250);
+ --sidebar-primary-foreground: oklch(0.99 0 0);
+ --sidebar-accent: oklch(0.94 0.01 250);
+ --sidebar-accent-foreground: oklch(0.28 0.05 250);
+ --sidebar-border: oklch(0.88 0.015 250);
+ --sidebar-ring: oklch(0.55 0.08 250);
+}
+
+.dark {
+ /* Dark Mode - Professional dark theme for research */
+ --background: oklch(0.12 0.02 250); /* Deep navy-black */
+ --foreground: oklch(0.98 0.005 250); /* Soft white */
+
+ /* Cards with subtle depth */
+ --card: oklch(0.16 0.03 250); /* Slightly lighter than background */
+ --card-foreground: oklch(0.98 0.005 250);
+
+ --popover: oklch(0.16 0.03 250);
+ --popover-foreground: oklch(0.98 0.005 250);
+
+ /* Primary - Bright but professional blue */
+ --primary: oklch(0.75 0.12 250); /* Clear, readable blue */
+ --primary-foreground: oklch(0.12 0.02 250); /* Dark background */
+
+ /* Secondary - Medium dark gray */
+ --secondary: oklch(0.2 0.03 250);
+ --secondary-foreground: oklch(0.98 0.005 250);
+
+ /* Muted - Professional dark grays */
+ --muted: oklch(0.2 0.03 250);
+ --muted-foreground: oklch(0.65 0.03 250); /* Medium gray text */
+
+ /* Accent - Teal that works well in dark mode */
+ --accent: oklch(0.3 0.08 190);
+ --accent-foreground: oklch(0.98 0.005 250);
+
+ /* Destructive - Clear but not overwhelming */
+ --destructive: oklch(0.65 0.18 25);
+
+ /* Borders - Defined but subtle */
+ --border: oklch(0.25 0.04 250);
+ --input: oklch(0.2 0.03 250);
+ --ring: oklch(0.75 0.12 250); /* Match primary */
+
+ /* Charts - Optimized for dark background */
+ --chart-1: oklch(0.7 0.15 250); /* Bright navy */
+ --chart-2: oklch(0.75 0.18 190); /* Bright teal */
+ --chart-3: oklch(0.72 0.2 320); /* Bright purple */
+ --chart-4: oklch(0.78 0.16 80); /* Bright olive */
+ --chart-5: oklch(0.68 0.14 30); /* Bright rust */
+
+ /* Sidebar */
+ --sidebar: oklch(0.14 0.025 250);
+ --sidebar-foreground: oklch(0.98 0.005 250);
+ --sidebar-primary: oklch(0.75 0.12 250);
+ --sidebar-primary-foreground: oklch(0.12 0.02 250);
+ --sidebar-accent: oklch(0.2 0.03 250);
+ --sidebar-accent-foreground: oklch(0.98 0.005 250);
+ --sidebar-border: oklch(0.25 0.04 250);
+ --sidebar-ring: oklch(0.65 0.08 250);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ font-feature-settings:
+ "rlig" 1,
+ "calt" 1;
+ }
+}
diff --git a/app-next/tsconfig.json b/app-next/tsconfig.json
new file mode 100644
index 00000000..cf9c65d3
--- /dev/null
+++ b/app-next/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/app/package.json b/app/package.json
index 12ab2785..645c7051 100644
--- a/app/package.json
+++ b/app/package.json
@@ -6,9 +6,9 @@
"private": false,
"license": "https://opensource.org/license/bsd-3-clause/",
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev --turbopack -p 3001",
"build": "next build",
- "start": "next start",
+ "start": "next start -p 3001",
"lint": "next lint"
},
"browserslist": [
@@ -62,10 +62,10 @@
"next-i18next": "^15.4.2",
"npm": "^10.9.2",
"polished": "4.3.1",
- "react": "19.1.0",
+ "react": "^18.3.0",
"react-chartjs-2": "5.3.0",
- "react-dom": "19.1.0",
- "react-helmet-async": "2.0.5",
+ "react-dom": "^18.3.0",
+ "react-helmet-async": "^2.0.5",
"react-i18next": "^15.5.1",
"react-markdown": "^10.1.0",
"react-perfect-scrollbar": "1.5.8",
diff --git a/app/src/components/Wrapper.js b/app/src/components/Wrapper.js
index f5153958..dd011013 100644
--- a/app/src/components/Wrapper.js
+++ b/app/src/components/Wrapper.js
@@ -6,9 +6,13 @@ import { useTheme } from "@mui/material/styles";
const WrapBox = styled(Box)`
display: flex;
justify-content: center;
- align: center;
- max-width: 1600px;
- margin: auto;
+ align-items: center;
+ width: 100%;
+ max-width: 100%;
+ margin: 0 auto;
+ padding-left: 24px;
+ padding-right: 24px;
+ box-sizing: border-box;
`;
const Wrapper = ({ children, fullWidth }) => {
@@ -16,10 +20,7 @@ const Wrapper = ({ children, fullWidth }) => {
const isLgUp = useMediaQuery(theme.breakpoints.up("lg"));
const isSmUp = useMediaQuery(theme.breakpoints.up("sm"));
return (
-
+
{children}
);
diff --git a/app/src/components/search/DatasetSearchResults.jsx b/app/src/components/search/DatasetSearchResults.jsx
new file mode 100644
index 00000000..28e3301a
--- /dev/null
+++ b/app/src/components/search/DatasetSearchResults.jsx
@@ -0,0 +1,212 @@
+/**
+ * DatasetSearchResults Component
+ *
+ * Displays the search results for datasets in different views:
+ * - List view (cards)
+ * - Table view (data grid)
+ * - Grid view (compact cards)
+ *
+ * This component is specifically for datasets and can be customized
+ * independently from other search types (flows, tasks, runs)
+ */
+
+import React from "react";
+import { Grid, Box, Typography, Alert } from "@mui/material";
+import { WithSearch } from "@elastic/react-search-ui";
+import ResultCard from "./ResultCard";
+import ResultGridCard from "./ResultGridCard";
+import ResultsTable from "./ResultTable";
+
+/**
+ * EmptyState - Shown when there are no results
+ */
+const EmptyState = ({ searchTerm, hasFilters }) => (
+
+
+ No datasets found
+
+ {searchTerm ? (
+
+ No results for "{searchTerm} "
+
+ ) : (
+
+ Try adjusting your filters or search terms
+
+ )}
+ {hasFilters && (
+
+ Try removing some filters to see more results
+
+ )}
+
+);
+
+/**
+ * ErrorState - Shown when there's an error loading results
+ */
+const ErrorState = ({ error }) => (
+
+
+
+ Error loading datasets
+
+
+ {error?.message || "Failed to load search results. Please try again."}
+
+
+ If the problem persists, check that Elasticsearch is accessible.
+
+
+
+);
+
+/**
+ * ListView - Displays results as cards (one per row)
+ */
+const ListView = ({ results, searchTerm }) => {
+ console.log("π ListView: Rendering", results?.length, "results");
+
+ if (!results || results.length === 0) {
+ return ;
+ }
+
+ return (
+
+ {results.map((result) => (
+
+
+
+ ))}
+
+ );
+};
+
+/**
+ * TableView - Displays results in a data grid table
+ */
+const TableView = ({ results, columns, searchTerm }) => {
+ console.log("π TableView: Rendering", results?.length, "results");
+
+ if (!results || results.length === 0) {
+ return ;
+ }
+
+ return ;
+};
+
+/**
+ * GridView - Displays results as a grid of compact cards
+ */
+const GridView = ({ results, searchTerm }) => {
+ console.log("π¨ GridView: Rendering", results?.length, "results");
+
+ if (!results || results.length === 0) {
+ return ;
+ }
+
+ return (
+
+ {results.map((result) => (
+
+ ))}
+
+ );
+};
+
+/**
+ * DatasetSearchResults - Main component that switches between views
+ *
+ * Props:
+ * - view: "list" | "table" | "grid"
+ * - columns: Array of column definitions for table view
+ */
+const DatasetSearchResults = ({ view, columns }) => {
+ return (
+ ({
+ results,
+ searchTerm,
+ filters,
+ wasSearched,
+ error,
+ })}
+ >
+ {({ results, searchTerm, filters, wasSearched, error }) => {
+ // Debug logging - check browser console
+ console.log("π DatasetSearchResults Debug:", {
+ view,
+ resultsCount: results?.length || 0,
+ searchTerm,
+ filtersCount: Object.keys(filters || {}).length,
+ wasSearched,
+ error,
+ firstResult: results?.[0],
+ });
+
+ // Show error state
+ if (error) {
+ console.error("β Search Error:", error);
+ return ;
+ }
+
+ // Show empty state if search was performed but no results
+ if (wasSearched && (!results || results.length === 0)) {
+ return (
+ 0}
+ />
+ );
+ }
+
+ // Show message before first search
+ if (!wasSearched) {
+ return (
+
+
+ Enter a search term or browse all datasets
+
+
+ );
+ }
+
+ // Render appropriate view
+ switch (view) {
+ case "table":
+ return (
+
+ );
+ case "grid":
+ return ;
+ case "list":
+ default:
+ return ;
+ }
+ }}
+
+ );
+};
+
+DatasetSearchResults.displayName = "DatasetSearchResults";
+
+export default DatasetSearchResults;
diff --git a/app/src/components/search/ResultCard.js b/app/src/components/search/ResultCard.js
index 31b8e9e6..68ad1ca6 100644
--- a/app/src/components/search/ResultCard.js
+++ b/app/src/components/search/ResultCard.js
@@ -33,6 +33,7 @@ import {
} from "./runCard";
import { faHashtag, faHistory } from "@fortawesome/free-solid-svg-icons";
+import { fontSize } from "@mui/system";
const SearchResultCard = styled(Card)`
height: 100%;
@@ -133,7 +134,7 @@ const ResultCard = ({ result }) => {
const theme = useTheme();
const router = useRouter();
- const type = result._meta.rawHit._type;
+ const type = result._meta?.rawHit?._type || "data";
const color = theme.palette.entity[type];
const icon = theme.palette.icon[type];
@@ -154,16 +155,25 @@ const ResultCard = ({ result }) => {
fullwidth={fullwidth}
variant="outlined"
>
-
+
-
+
+
+
{stats !== undefined && (
{stats.map((stat, index) => (
-
+
{
))}
)}
-
-
+
+
-
+
-
+
{" "}
{result.id.raw}
diff --git a/app/src/components/search/ResultGridCard.js b/app/src/components/search/ResultGridCard.js
index eaad5f6f..c5d0d708 100644
--- a/app/src/components/search/ResultGridCard.js
+++ b/app/src/components/search/ResultGridCard.js
@@ -7,8 +7,11 @@ const ResultGridCard = ({ result }) => {
diff --git a/app/src/components/search/SearchContainer.js b/app/src/components/search/SearchContainer.js
index 6adf1b85..7ee37bcb 100644
--- a/app/src/components/search/SearchContainer.js
+++ b/app/src/components/search/SearchContainer.js
@@ -20,6 +20,7 @@ import { TabContext, TabPanel } from "@mui/lab";
import ResultCard from "./ResultCard";
import ResultGridCard from "./ResultGridCard";
import Sort from "./Sort";
+import DatasetSearchResults from "./DatasetSearchResults";
import { styled } from "@mui/material/styles";
import {
@@ -178,14 +179,55 @@ const SearchContainer = memo(
if (newView !== null) {
setView(newView);
}
-
-
};
return (
- ({ isLoading })}>
- {({ isLoading }) => (isLoading ? : null)}
+ ({
+ isLoading,
+ wasSearched,
+ resultSearchTerm,
+ totalResults,
+ error,
+ })}
+ >
+ {({
+ isLoading,
+ wasSearched,
+ resultSearchTerm,
+ totalResults,
+ error,
+ }) => {
+ // Debug logging
+ console.log("π SearchContainer Debug:", {
+ isLoading,
+ wasSearched,
+ resultSearchTerm,
+ totalResults,
+ error,
+ configKeys: Object.keys(config || {}),
+ });
+
+ return (
+ <>
+ {isLoading && }
+ {error && (
+
+
+ Error: {error.message || "Failed to load search results"}
+
+
+ )}
+ >
+ );
+ }}
{false && }
@@ -223,8 +265,8 @@ const SearchContainer = memo(
-
-
+
+
-
- {view === "list" && (
-
- )}
- {view === "table" && (
- ({ results })}>
- {({ results }) => (
-
- )}
-
- )}
- {view === "grid" && (
- ({ results })}>
- {({ results }) => (
-
- {results.map((res, i) => (
-
- ))}
-
- )}
-
- )}
+
+ {/* Use the new DatasetSearchResults component */}
+
-
+
{
function Sort({ sortOptions, setSort, sortList }) {
// Handle sort direction toggle
- const [sortDirection, setSortDirection] = useState("desc");
+ const sortDirection =
+ sortList.length > 0 && sortList[0]["direction"] === "asc" ? "asc" : "desc";
+
const toggleSortDirection = () => {
- const newDirection =
- sortList.length > 0 && sortList[0]["direction"] === "asc"
- ? "desc"
- : "asc";
+ const newDirection = sortDirection === "asc" ? "desc" : "asc";
let newSortList = [];
if (sortList.length > 0) {
@@ -62,7 +61,6 @@ function Sort({ sortOptions, setSort, sortList }) {
}
setSort(newSortList); // Update the Search UI state
- setSortDirection(newDirection); // Update the local state (for button)
};
//Translate sort options
diff --git a/app/src/components/search/Teaser.js b/app/src/components/search/Teaser.js
index 424a40ff..bf7d7f42 100644
--- a/app/src/components/search/Teaser.js
+++ b/app/src/components/search/Teaser.js
@@ -3,7 +3,7 @@ import ReactMarkdown from "react-markdown";
const MarkdownWrapper = styled("div")(({ theme }) => ({
marginBottom: "10px",
- fontSize: "12px",
+ fontSize: "inherit", // Inherit font size from parent instead of hardcoding
overflow: "hidden",
display: "-webkit-box",
WebkitBoxOrient: "vertical",
@@ -16,7 +16,7 @@ const MarkdownWrapper = styled("div")(({ theme }) => ({
backgroundColor: "rgba(0, 0, 0, 0.1)",
padding: "2px",
borderRadius: "4px",
- fontSize: "12px",
+ fontSize: "inherit",
fontFamily: "'Roboto Mono', monospace",
},
}));
diff --git a/app/src/components/search/dataCard.js b/app/src/components/search/dataCard.js
index d37dba80..576a23fd 100644
--- a/app/src/components/search/dataCard.js
+++ b/app/src/components/search/dataCard.js
@@ -55,13 +55,16 @@ const status = {
export const Title = ({ result }) => {
return (
- {result.name.raw}
- v.{result.version.raw}
-
+ {result?.name?.raw || "Unnamed"}
+ v.{result?.version?.raw || "0"}
+
@@ -115,7 +118,7 @@ export const Description = ({ result }) => {
return (
{
return (
- {shortenName(result.name.raw)}
+ {shortenName(result.run_flow.raw.name)} on{" "}
+ {result.run_task.raw.source_data.name} by {result.uploader.raw}
);
diff --git a/app/src/components/search/taskCard.js b/app/src/components/search/taskCard.js
index 420f30bd..9e4b0b3b 100644
--- a/app/src/components/search/taskCard.js
+++ b/app/src/components/search/taskCard.js
@@ -12,7 +12,9 @@ export const Title = ({ result }) => {
return (
- {result.tasktype.raw.name} on {result.source_data.raw.name}
+ {shortenName(result.run_flow.raw.name)} on{" "}
+ {result.run_task.raw.source_data.name} by {result.uploader.raw}
+ {/* {result.tasktype.raw.name} on {result.source_data.raw.name} */}
);
diff --git a/app/src/components/sidebar/SidebarNav.js b/app/src/components/sidebar/SidebarNav.js
index 3084a5f2..f2559362 100644
--- a/app/src/components/sidebar/SidebarNav.js
+++ b/app/src/components/sidebar/SidebarNav.js
@@ -6,19 +6,25 @@ import useMediaQuery from "@mui/material/useMediaQuery";
import SidebarNavSection from "./SidebarNavSection";
-const Scrollbar = styled('div')(({ theme }) => ({
- backgroundColor: theme.palette.sidebar?.background || theme.palette.background.default || "#FFF",
- borderRight: '1px solid rgba(0, 0, 0, 0.12)',
+const Scrollbar = styled("div")(({ theme }) => ({
+ backgroundColor:
+ theme.palette.sidebar?.background ||
+ theme.palette.background.default ||
+ "#FFF",
+ borderRight: "1px solid rgba(0, 0, 0, 0.12)",
flexGrow: 1,
}));
const PerfectScrollbarWrapper = styled(ReactPerfectScrollbar)(({ theme }) => ({
- backgroundColor: theme.palette.sidebar?.background || theme.palette.background.default || "#FFF",
- borderRight: '1px solid rgba(0, 0, 0, 0.12)',
+ backgroundColor:
+ theme.palette.sidebar?.background ||
+ theme.palette.background.default ||
+ "#FFF",
+ borderRight: "1px solid rgba(0, 0, 0, 0.12)",
flexGrow: 1,
}));
-const Items = styled('div')(({ theme }) => ({
+const Items = styled("div")(({ theme }) => ({
paddingTop: theme.spacing(2.5),
paddingBottom: theme.spacing(2.5),
}));
@@ -33,11 +39,15 @@ const SidebarNav = ({ items }) => {
fetch("/api/count")
.then((response) => response.json())
.then((data) => {
- const counts = data.reduce((acc, item) => {
- acc[item.index] = item.count;
- return acc;
- }, {});
- setCount(counts);
+ if (Array.isArray(data)) {
+ const counts = data.reduce((acc, item) => {
+ acc[item.index] = item.count;
+ return acc;
+ }, {});
+ setCount(counts);
+ } else {
+ console.error("Expected array but got:", data);
+ }
})
.catch((error) => {
console.error("Error fetching count:", error);
@@ -63,4 +73,4 @@ const SidebarNav = ({ items }) => {
);
};
-export default SidebarNav;
\ No newline at end of file
+export default SidebarNav;
diff --git a/app/src/components/sidebar/SidebarNavListItem.js b/app/src/components/sidebar/SidebarNavListItem.js
index 2bb44773..521d9ea9 100644
--- a/app/src/components/sidebar/SidebarNavListItem.js
+++ b/app/src/components/sidebar/SidebarNavListItem.js
@@ -118,7 +118,10 @@ const SidebarNavListItem = (props) => {
const { pathname } = useRouter();
const isExternal = href[0] != "/" ? true : false;
- const key = isExternal && !href.includes("openml.org") ? href?.split("/").pop() : href?.split(/[./]+/)[1];
+ const key =
+ isExternal && !href.includes("openml.org")
+ ? href?.split("/").pop()
+ : href?.split(/[./]+/)[1];
const color = theme.palette.entity[key];
const icon = theme.palette.icon[key];
diff --git a/app/src/contexts/ThemeContext.js b/app/src/contexts/ThemeContext.js
index 8c8b41a2..fe0c6259 100644
--- a/app/src/contexts/ThemeContext.js
+++ b/app/src/contexts/ThemeContext.js
@@ -1,7 +1,14 @@
import React, { useEffect, useState } from "react";
+// Define THEMES constant
+const THEMES = {
+ DEFAULT: "light", // or "dark" - choose your default
+ LIGHT: "light",
+ DARK: "dark",
+};
+
const initialState = {
- theme: "DEFAULT",
+ theme: THEMES.DEFAULT,
setTheme: () => {},
};
@@ -38,4 +45,4 @@ function ThemeProvider({ children }) {
);
}
-export { ThemeProvider, ThemeContext };
+export { ThemeProvider, ThemeContext, THEMES };
diff --git a/app/src/layouts/Dashboard.js b/app/src/layouts/Dashboard.js
index 76360073..cebc0fb8 100644
--- a/app/src/layouts/Dashboard.js
+++ b/app/src/layouts/Dashboard.js
@@ -30,6 +30,8 @@ const AppContent = styled(Box)`
display: flex;
flex-direction: column;
max-width: 100%;
+ width: 100%;
+ padding: 0;
`;
const Paper = styled(MuiPaper)(spacing);
diff --git a/app/src/pages/api/autocomplete.js b/app/src/pages/api/autocomplete.js
index 8128487c..aa3dab89 100644
--- a/app/src/pages/api/autocomplete.js
+++ b/app/src/pages/api/autocomplete.js
@@ -1,7 +1,10 @@
import axios from "axios";
-
const ELASTICSEARCH_SERVER = "https://www.openml.org/es/";
+// export default async function handler(req, res) {
+// const elasticsearchEndpoint = `${ELASTICSEARCH_SERVER}/_msearch`;
+// const indices = ["data", "task", "flow", "run", "study", "measure"];
+
export default async function handler(req, res) {
const { requestState, queryConfig, indexName } = req.body;
const searchTerm = requestState?.searchTerm;
@@ -16,18 +19,18 @@ export default async function handler(req, res) {
match: {
name: {
query: searchTerm,
- fuzziness: "AUTO"
- }
- }
+ fuzziness: "AUTO",
+ },
+ },
},
size: queryConfig?.autocompleteQuery?.resultsPerPage || 5,
- _source: ["name", "url"]
+ _source: ["name", "url"],
};
const response = await axios.post(
- `${ELASTICSEARCH_SERVER}/${indexName}/_search`,
+ `${ELASTICSEARCH_SERVER}/${indexName}/_msearch`,
esQuery,
- { headers: { "Content-Type": "application/json" } }
+ { headers: { "Content-Type": "application/json" } },
);
const hits = response.data.hits.hits || [];
@@ -35,7 +38,7 @@ export default async function handler(req, res) {
const autocompletedResults = hits.map((hit) => ({
id: { raw: hit._id },
title: { snippet: hit._source.name },
- url: { raw: hit._source.url }
+ url: { raw: hit._source.url },
}));
res.status(200).json({
diff --git a/app/src/pages/api/search.js b/app/src/pages/api/search.js
index 29394b61..1438415b 100644
--- a/app/src/pages/api/search.js
+++ b/app/src/pages/api/search.js
@@ -1,30 +1,91 @@
import ElasticsearchAPIConnector from "@elastic/search-ui-elasticsearch-connector";
const connectorsCache = {};
-
-// Set to true if you want to use the dev proxy
-// This requires starting server-proxy app with `node server.js`
const use_dev_proxy = false;
export default async function handler(req, res) {
- const { requestState, queryConfig, indexName } = req.body;
-
- if (!connectorsCache[indexName]) {
- connectorsCache[indexName] = new ElasticsearchAPIConnector({
- host: use_dev_proxy
- ? "http://localhost:3001/proxy"
- : "https://www.openml.org/es/",
- index: indexName,
- apiKey: "",
+ try {
+ const { requestState, queryConfig, indexName } = req.body;
+
+ // Enhanced logging
+ console.log("π /api/search called:", {
+ indexName,
+ hasRequestState: !!requestState,
+ hasQueryConfig: !!queryConfig,
+ searchTerm: requestState?.searchTerm,
+ filters: requestState?.filters,
});
- }
- //This runs server-side, so the output appears in the terminal
- //console.log("OnSearch", indexName, requestState, queryConfig);
- const response = await connectorsCache[indexName].onSearch(
- requestState,
- queryConfig,
- );
- //console.log(response);
- res.json(response);
+ if (!indexName || !requestState) {
+ console.error("β Missing required fields:", {
+ indexName,
+ hasRequestState: !!requestState,
+ });
+ return res.status(400).json({
+ error: "Missing required fields",
+ details: {
+ indexName: !!indexName,
+ requestState: !!requestState,
+ },
+ });
+ }
+
+ if (!connectorsCache[indexName]) {
+ console.log(
+ "π¦ Creating new Elasticsearch connector for index:",
+ indexName,
+ );
+ try {
+ connectorsCache[indexName] = new ElasticsearchAPIConnector({
+ host: use_dev_proxy
+ ? "http://localhost:3001/proxy"
+ : "https://www.openml.org/es/",
+ index: indexName,
+ apiKey: "",
+ });
+ console.log("β
Connector created successfully");
+ } catch (connectorError) {
+ console.error("β Failed to create connector:", connectorError);
+ throw new Error(
+ `Failed to create Elasticsearch connector: ${connectorError.message}`,
+ );
+ }
+ }
+
+ // Add timeout
+ console.log("π Executing search...");
+ const searchPromise = connectorsCache[indexName].onSearch(
+ requestState,
+ queryConfig,
+ );
+
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error("Search request timeout after 10s")),
+ 10000,
+ ),
+ );
+
+ const response = await Promise.race([searchPromise, timeoutPromise]);
+
+ // Log successful response
+ console.log("β
Search successful:", {
+ resultsCount: response?.results?.length || 0,
+ totalResults: response?.totalResults || 0,
+ hasResults: !!response?.results,
+ });
+
+ res.status(200).json(response);
+ } catch (error) {
+ console.error("β Error in /api/search:", {
+ message: error.message,
+ stack: error.stack,
+ name: error.name,
+ });
+ res.status(500).json({
+ error: error.message,
+ type: error.name,
+ details: "Check server logs for more information",
+ });
+ }
}
diff --git a/app/src/pages/api/test-es.js b/app/src/pages/api/test-es.js
new file mode 100644
index 00000000..40a5e9a2
--- /dev/null
+++ b/app/src/pages/api/test-es.js
@@ -0,0 +1,84 @@
+/**
+ * Elasticsearch Connection Test API
+ *
+ * Tests if we can reach Elasticsearch
+ * Navigate to: http://localhost:3001/api/test-es
+ */
+
+export default async function handler(req, res) {
+ try {
+ console.log("π§ͺ Testing Elasticsearch connection...");
+
+ const esUrl = "https://www.openml.org/es/";
+
+ // Test 1: Can we reach ES?
+ const response = await fetch(esUrl, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const data = await response.json();
+
+ console.log("β
Elasticsearch reachable:", {
+ status: response.status,
+ clusterName: data.cluster_name,
+ version: data.version?.number,
+ });
+
+ // Test 2: Can we query the data index?
+ const searchUrl = `${esUrl}data/_search`;
+ const searchResponse = await fetch(searchUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ size: 5,
+ query: {
+ match_all: {},
+ },
+ }),
+ });
+
+ const searchData = await searchResponse.json();
+
+ console.log("β
Search test results:", {
+ status: searchResponse.status,
+ totalHits: searchData.hits?.total?.value || searchData.hits?.total || 0,
+ firstResult: searchData.hits?.hits?.[0]?._source?.name || "No results",
+ });
+
+ return res.status(200).json({
+ success: true,
+ elasticsearch: {
+ reachable: true,
+ url: esUrl,
+ clusterName: data.cluster_name,
+ version: data.version?.number,
+ },
+ search: {
+ indexName: "data",
+ totalHits: searchData.hits?.total?.value || searchData.hits?.total || 0,
+ sampleResults:
+ searchData.hits?.hits?.slice(0, 3).map((hit) => ({
+ id: hit._source.data_id,
+ name: hit._source.name,
+ })) || [],
+ },
+ });
+ } catch (error) {
+ console.error("β Elasticsearch test failed:", {
+ message: error.message,
+ stack: error.stack,
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: error.message,
+ stack: error.stack,
+ suggestion: "Check if Elasticsearch is accessible from your server",
+ });
+ }
+}
diff --git a/app/src/pages/api/test-search.js b/app/src/pages/api/test-search.js
new file mode 100644
index 00000000..731d1ad6
--- /dev/null
+++ b/app/src/pages/api/test-search.js
@@ -0,0 +1,74 @@
+/**
+ * Simple Search Test API
+ *
+ * Tests search without using the Elasticsearch connector library
+ * Navigate to: http://localhost:3001/api/test-search
+ */
+
+export default async function handler(req, res) {
+ try {
+ console.log("π§ͺ Testing simple search...");
+
+ const esUrl = "https://www.openml.org/es/data/_search";
+
+ const searchBody = {
+ from: 0,
+ size: 20,
+ query: {
+ match_all: {},
+ },
+ sort: [{ runs: { order: "desc" } }],
+ };
+
+ console.log("π€ Sending query:", JSON.stringify(searchBody, null, 2));
+
+ const response = await fetch(esUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(searchBody),
+ });
+
+ if (!response.ok) {
+ throw new Error(`ES returned ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ console.log("β
Search successful:", {
+ totalHits: data.hits?.total?.value || data.hits?.total || 0,
+ returnedHits: data.hits?.hits?.length || 0,
+ });
+
+ // Format results to match Search UI format
+ const results = data.hits.hits.map((hit) => ({
+ id: { raw: hit._source.data_id },
+ data_id: { raw: hit._source.data_id },
+ name: { raw: hit._source.name },
+ description: { raw: hit._source.description },
+ status: { raw: hit._source.status },
+ // Add more fields as needed
+ }));
+
+ return res.status(200).json({
+ success: true,
+ rawQuery: searchBody,
+ totalResults: data.hits?.total?.value || data.hits?.total || 0,
+ resultsCount: results.length,
+ results: results.slice(0, 5), // Just first 5 for testing
+ sampleResult: results[0],
+ });
+ } catch (error) {
+ console.error("β Simple search failed:", {
+ message: error.message,
+ stack: error.stack,
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: error.message,
+ stack: error.stack,
+ });
+ }
+}
diff --git a/app/src/pages/d/search.js b/app/src/pages/d/search.js
index 860a2599..660b7e5b 100644
--- a/app/src/pages/d/search.js
+++ b/app/src/pages/d/search.js
@@ -1,8 +1,13 @@
-import React from "react";
-import { useNextRouting } from "../../utils/useNextRouting";
-
+/**
+ * DEPRECATED: This route is kept for backward compatibility
+ * Please use /datasets instead
+ *
+ * This file redirects /d/search to /datasets
+ */
+import React, { useEffect } from "react";
+import { useRouter } from "next/router";
import DashboardLayout from "../../layouts/Dashboard";
-import SearchContainer from "../../components/search/SearchContainer";
+import { Box, Typography, CircularProgress } from "@mui/material";
import {
renderCell,
valueGetter,
@@ -29,243 +34,75 @@ import {
// Server-side translation
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+/**
+ * getStaticProps - Required for i18n but redirects immediately
+ */
export async function getStaticProps(context) {
- // extract the locale identifier from the URL
const { locale } = context;
return {
props: {
- // pass the translation props to the page component
...(await serverSideTranslations(locale)),
},
};
}
-// Defines chips in table view
-const getChipProps = (value) => {
- switch (value) {
- // Dataset status
- case "active":
- return {
- label: "Verified",
- icon: ,
- color: "success",
- };
- case "deactivated":
- return {
- label: "Deprecated",
- icon: ,
- color: "error",
- };
- case "in_preparation":
- return {
- label: "In preparation",
- icon: ,
- color: "warning",
- };
- // Dataset licence
- case "public":
- case "Public":
- return {
- label: "Public",
- icon: ,
- color: "success",
- };
- case "CC0":
- case "CCZero":
- case "CC0: Public Domain":
- case "Public Domain (CC0)":
- return {
- label: "CC0",
- icon: ,
- color: "success",
- };
- case "CC BY 4.0":
- case "CC BY-NC-ND":
- case "CC BY-NC 4.0":
- case "CC BY-SA":
- case "CC BY":
- case "Creative Commons Attribution":
- return {
- label: value,
- icon: ,
- color: "primary",
- };
- case "Open Database License (ODbL)":
- return {
- label: "ODbL",
- icon: ,
- color: "success",
- };
- default:
- return {
- label: value,
- };
- }
-};
-
-const sort_options = [
- {
- name: "search.relevance",
- value: [],
- },
- {
- name: "search.most_runs",
- value: [{ field: "runs", direction: "desc" }],
- },
- {
- name: "search.most_likes",
- value: [{ field: "nr_of_likes", direction: "desc" }],
- },
- {
- name: "search.most_downloads",
- value: [{ field: "nr_of_downloads", direction: "desc" }],
- },
- {
- name: "search.most_recent",
- value: [{ field: "date", direction: "desc" }],
- },
- {
- name: "search.most_instances",
- value: [{ field: "qualities.NumberOfInstances", direction: "desc" }],
- },
- {
- name: "search.most_features",
- value: [{ field: "qualities.NumberOfFeatures", direction: "desc" }],
- },
- {
- name: "search.most_numeric_features",
- value: [{ field: "qualities.NumberOfNumericFeatures", direction: "desc" }],
- },
- {
- name: "search.most_missing_values",
- value: [{ field: "qualities.NumberOfMissing values", direction: "desc" }],
- },
- {
- name: "search.most_classes",
- value: [{ field: "qualities.NumberOfClasses", direction: "desc" }],
- },
-];
-
-const search_facets = [
- {
- label: "filters.status",
- field: "status.keyword",
- },
- {
- label: "filters.licence",
- field: "licence.keyword",
- },
- {
- label: "filters.size",
- field: "qualities.NumberOfInstances",
- },
- {
- label: "filters.features",
- field: "qualities.NumberOfFeatures",
- },
- {
- label: "filters.target",
- field: "qualities.NumberOfClasses",
- },
- {
- label: "filters.format",
- field: "format",
- },
-];
+/**
+ * DataSearchRedirect - Redirects users to the new /datasets route
+ *
+ * Why redirect instead of just updating the route?
+ * - Old links and bookmarks still work
+ * - Search engines get told about the new URL
+ * - Users see a brief message explaining the redirect
+ *
+ * How it works:
+ * 1. Component mounts
+ * 2. useEffect runs and initiates redirect
+ * 3. Query parameters are preserved (e.g., ?status=active)
+ */
+function DataSearchRedirect() {
+ const router = useRouter();
-// Controls how columns are rendered and manipulated in the table view
-const columns = [
- {
- field: "data_id",
- headerName: "Data_id",
- valueGetter: valueGetter("data_id"),
- renderCell: renderCell,
- width: 70,
- },
- {
- field: "name",
- headerName: "Name",
- valueGetter: valueGetter("name"),
- renderCell: renderCell,
- width: 230,
- },
- {
- field: "version",
- headerName: "Version",
- valueGetter: valueGetter("version"),
- renderCell: renderCell,
- width: 60,
- },
- {
- field: "status",
- headerName: "Status",
- valueGetter: valueGetter("status"),
- renderCell: renderChips(getChipProps),
- type: "singleSelect",
- valueOptions: ["active", "deactivated", "in_preparation"],
- width: 136,
- },
- {
- field: "description",
- headerName: "Description",
- valueGetter: valueGetter("description"),
- renderCell: renderDescription,
- width: 360,
- },
- {
- field: "date",
- headerName: "Date",
- valueGetter: valueGetter("date"),
- renderCell: renderDate,
- },
- {
- field: "licence",
- headerName: "Licence",
- valueGetter: valueGetter("licence"),
- renderCell: renderChips(getChipProps),
- width: 110,
- },
- {
- field: "creator",
- headerName: "Creator",
- valueGetter: valueGetter("creator"),
- renderCell: renderCell,
- width: 150,
- },
- {
- field: "url",
- headerName: "Url",
- valueGetter: valueGetter("url"),
- renderCell: copyCell,
- copyMessage: true,
- width: 50,
- },
- {
- field: "tags",
- headerName: "Tags",
- valueGetter: valueGetter("tags"),
- renderCell: renderTags,
- width: 400,
- },
-];
+ useEffect(() => {
+ // Preserve query parameters when redirecting
+ // Example: /d/search?status=active β /datasets?status=active
+ const query = router.query;
-function DataSearchContainer() {
- const combinedConfig = useNextRouting(dataConfig, "");
+ // Use replace instead of push to not add to browser history
+ // This means users can't go "back" to the old URL
+ router.replace({
+ pathname: "/datasets",
+ query: query, // Keep all query parameters
+ });
+ }, [router]);
+ // Show a loading message while redirecting
return (
-
+
+
+
+ Redirecting to /datasets...
+
+
+ This URL has been moved for better SEO
+
+
);
}
-DataSearchContainer.getLayout = function getLayout(page) {
+/**
+ * getLayout - Still use DashboardLayout for consistent navigation
+ */
+DataSearchRedirect.getLayout = function getLayout(page) {
return {page} ;
};
-DataSearchContainer.displayName = "DataSearchContainer";
+DataSearchRedirect.displayName = "DataSearchRedirect";
-export default DataSearchContainer;
+export default DataSearchRedirect;
diff --git a/app/src/pages/datasets.js b/app/src/pages/datasets.js
new file mode 100644
index 00000000..ae32a983
--- /dev/null
+++ b/app/src/pages/datasets.js
@@ -0,0 +1,389 @@
+/**
+ * Dataset Search Page - SEO Optimized
+ *
+ * This page displays a searchable list of datasets from OpenML
+ * Route: /datasets
+ *
+ * Key Concepts:
+ * 1. getStaticProps - Pre-renders this page at build time (good for SEO)
+ * 2. serverSideTranslations - Loads translations for internationalization
+ * 3. SearchContainer - Reusable component that handles all search logic
+ */
+
+import React from "react";
+import { useNextRouting } from "../utils/useNextRouting";
+import Head from "next/head";
+
+import DashboardLayout from "../layouts/Dashboard";
+import SearchContainer from "../components/search/SearchContainer";
+import {
+ renderCell,
+ valueGetter,
+ copyCell,
+ renderDescription,
+ renderDate,
+ renderTags,
+ renderChips,
+} from "../components/search/ResultTable";
+
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ faCheck,
+ faTriangleExclamation,
+ faRotate,
+} from "@fortawesome/free-solid-svg-icons";
+
+import dataConfig from "../search_configs/dataConfig";
+import {
+ faCreativeCommonsBy,
+ faCreativeCommonsPd,
+ faCreativeCommonsZero,
+} from "@fortawesome/free-brands-svg-icons";
+
+// Server-side translation
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+/**
+ * getStaticProps - Next.js function that runs at build time
+ *
+ * What it does:
+ * - Loads translation files for the current locale (language)
+ * - Runs on the SERVER, not in the browser
+ * - Makes the page load faster and better for SEO
+ *
+ * @param {object} context - Contains locale info (en, de, fr, etc.)
+ * @returns {object} Props to pass to the page component
+ */
+export async function getStaticProps(context) {
+ const { locale } = context; // Get current language (e.g., 'en', 'de')
+
+ return {
+ props: {
+ // Pass translations to the page
+ ...(await serverSideTranslations(locale)),
+ },
+ };
+}
+
+/**
+ * getChipProps - Defines how status/license chips look
+ *
+ * This function maps dataset status and license types to visual chips
+ * with icons and colors
+ *
+ * @param {string} value - The status or license type
+ * @returns {object} Chip configuration (label, icon, color)
+ */
+const getChipProps = (value) => {
+ switch (value) {
+ // Dataset status
+ case "active":
+ return {
+ label: "Verified",
+ icon: ,
+ color: "success",
+ };
+ case "deactivated":
+ return {
+ label: "Deprecated",
+ icon: ,
+ color: "error",
+ };
+ case "in_preparation":
+ return {
+ label: "In preparation",
+ icon: ,
+ color: "warning",
+ };
+ // Dataset licence
+ case "public":
+ case "Public":
+ return {
+ label: "Public",
+ icon: ,
+ color: "success",
+ };
+ case "CC0":
+ case "CCZero":
+ case "CC0: Public Domain":
+ case "Public Domain (CC0)":
+ return {
+ label: "CC0",
+ icon: ,
+ color: "success",
+ };
+ case "CC BY 4.0":
+ case "CC BY-NC-ND":
+ case "CC BY-NC 4.0":
+ case "CC BY-SA":
+ case "CC BY":
+ case "Creative Commons Attribution":
+ return {
+ label: value,
+ icon: ,
+ color: "primary",
+ };
+ case "Open Database License (ODbL)":
+ return {
+ label: "ODbL",
+ icon: ,
+ color: "success",
+ };
+ default:
+ return {
+ label: value,
+ };
+ }
+};
+
+/**
+ * sort_options - Defines how users can sort search results
+ *
+ * Each option has:
+ * - name: Translation key for the label
+ * - value: Elasticsearch sort configuration
+ */
+const sort_options = [
+ {
+ name: "search.relevance",
+ value: [], // Default relevance sorting
+ },
+ {
+ name: "search.most_runs",
+ value: [{ field: "runs", direction: "desc" }],
+ },
+ {
+ name: "search.most_likes",
+ value: [{ field: "nr_of_likes", direction: "desc" }],
+ },
+ {
+ name: "search.most_downloads",
+ value: [{ field: "nr_of_downloads", direction: "desc" }],
+ },
+ {
+ name: "search.most_recent",
+ value: [{ field: "date", direction: "desc" }],
+ },
+ {
+ name: "search.most_instances",
+ value: [{ field: "qualities.NumberOfInstances", direction: "desc" }],
+ },
+ {
+ name: "search.most_features",
+ value: [{ field: "qualities.NumberOfFeatures", direction: "desc" }],
+ },
+ {
+ name: "search.most_numeric_features",
+ value: [{ field: "qualities.NumberOfNumericFeatures", direction: "desc" }],
+ },
+ {
+ name: "search.most_missing_values",
+ value: [{ field: "qualities.NumberOfMissingValues", direction: "desc" }],
+ },
+ {
+ name: "search.most_classes",
+ value: [{ field: "qualities.NumberOfClasses", direction: "desc" }],
+ },
+];
+
+/**
+ * search_facets - Defines filter options in the sidebar
+ *
+ * Facets allow users to filter results by:
+ * - Status (active, deactivated, etc.)
+ * - License type
+ * - Number of instances (size)
+ * - Number of features
+ * - Number of classes (target)
+ * - File format
+ */
+const search_facets = [
+ {
+ label: "filters.status",
+ field: "status.keyword",
+ },
+ {
+ label: "filters.licence",
+ field: "licence.keyword",
+ },
+ {
+ label: "filters.size",
+ field: "qualities.NumberOfInstances",
+ },
+ {
+ label: "filters.features",
+ field: "qualities.NumberOfFeatures",
+ },
+ {
+ label: "filters.target",
+ field: "qualities.NumberOfClasses",
+ },
+ {
+ label: "filters.format",
+ field: "format",
+ },
+];
+
+/**
+ * columns - Defines the table columns for dataset search results
+ *
+ * Each column has:
+ * - field: The data field from Elasticsearch
+ * - headerName: Display name in the table header
+ * - valueGetter: Function to extract the value
+ * - renderCell: Function to render the cell content
+ * - width: Column width in pixels
+ */
+const columns = [
+ {
+ field: "data_id",
+ headerName: "Data_id",
+ valueGetter: valueGetter("data_id"),
+ renderCell: renderCell,
+ width: 70,
+ },
+ {
+ field: "name",
+ headerName: "Name",
+ valueGetter: valueGetter("name"),
+ renderCell: renderCell,
+ width: 230,
+ },
+ {
+ field: "version",
+ headerName: "Version",
+ valueGetter: valueGetter("version"),
+ renderCell: renderCell,
+ width: 60,
+ },
+ {
+ field: "status",
+ headerName: "Status",
+ valueGetter: valueGetter("status"),
+ renderCell: renderChips(getChipProps),
+ type: "singleSelect",
+ valueOptions: ["active", "deactivated", "in_preparation"],
+ width: 136,
+ },
+ {
+ field: "description",
+ headerName: "Description",
+ valueGetter: valueGetter("description"),
+ renderCell: renderDescription,
+ width: 360,
+ },
+ {
+ field: "date",
+ headerName: "Date",
+ valueGetter: valueGetter("date"),
+ renderCell: renderDate,
+ },
+ {
+ field: "licence",
+ headerName: "Licence",
+ valueGetter: valueGetter("licence"),
+ renderCell: renderChips(getChipProps),
+ width: 110,
+ },
+ {
+ field: "creator",
+ headerName: "Creator",
+ valueGetter: valueGetter("creator"),
+ renderCell: renderCell,
+ width: 150,
+ },
+ {
+ field: "url",
+ headerName: "Url",
+ valueGetter: valueGetter("url"),
+ renderCell: copyCell,
+ copyMessage: true,
+ width: 50,
+ },
+ {
+ field: "tags",
+ headerName: "Tags",
+ valueGetter: valueGetter("tags"),
+ renderCell: renderTags,
+ width: 400,
+ },
+];
+
+/**
+ * DatasetsPage - Main component for the datasets search page
+ *
+ * How it works:
+ * 1. Uses useNextRouting hook to handle URL query parameters
+ * 2. Passes configuration to SearchContainer
+ * 3. SearchContainer handles all the search logic, filters, pagination, etc.
+ *
+ * @returns {JSX.Element} The datasets search page
+ */
+function DatasetsPage() {
+ // useNextRouting hook synchronizes URL params with search state
+ // Example: /datasets?status=active&sort=date
+ const combinedConfig = useNextRouting(dataConfig, "/datasets");
+
+ return (
+ <>
+ {/* SEO Meta Tags - These help Google and other search engines */}
+
+ OpenML Datasets - Search Machine Learning Datasets
+
+
+
+ {/* Open Graph tags for social media sharing */}
+
+
+
+
+ {/* Canonical URL - tells search engines this is the main URL */}
+
+
+
+ {/* SearchContainer is a reusable component that handles:
+ - Elasticsearch queries
+ - Filters and facets
+ - Sorting
+ - Pagination
+ - Results display (grid, list, table views)
+ */}
+
+ >
+ );
+}
+
+/**
+ * getLayout - Wraps the page with DashboardLayout
+ *
+ * This is a Next.js pattern for persistent layouts.
+ * The DashboardLayout includes:
+ * - Navigation sidebar
+ * - Header
+ * - Footer
+ *
+ * This layout persists when navigating between pages,
+ * preventing unnecessary re-renders.
+ */
+DatasetsPage.getLayout = function getLayout(page) {
+ return {page} ;
+};
+
+// Helpful for debugging in React DevTools
+DatasetsPage.displayName = "DatasetsPage";
+
+export default DatasetsPage;
diff --git a/app/src/pages/f/search.js b/app/src/pages/f/search.js
index 994b85dd..00449f78 100644
--- a/app/src/pages/f/search.js
+++ b/app/src/pages/f/search.js
@@ -1,21 +1,34 @@
-import React from "react";
-import { useNextRouting } from "../../utils/useNextRouting";
-
-import DashboardLayout from "../../layouts/Dashboard";
-import SearchContainer from "../../components/search/SearchContainer";
-import {
- renderCell,
- valueGetter,
- renderDescription,
- renderDate,
- renderTags,
- renderChips,
-} from "../../components/search/ResultTable";
-
-import flowConfig from "../../search_configs/flowConfig";
+/**
+ * Flow Search Redirect Page
+ *
+ * This page redirects from the old /f/search URL to the new SEO-friendly /flows URL
+ * Preserves all query parameters during redirect
+ */
+
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+
+export default function FlowSearchRedirect() {
+ const router = useRouter();
+
+ useEffect(() => {
+ // Get all query parameters from current URL
+ const { query } = router;
+
+ // Redirect to /flows with the same query parameters
+ router.replace({
+ pathname: "/flows",
+ query: query,
+ });
+ }, [router]);
+
+ // Show nothing while redirecting
+ return null;
+}
// Server-side translation
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
export async function getStaticProps(context) {
// extract the locale identifier from the URL
const { locale } = context;
@@ -68,6 +81,14 @@ const search_facets = [
];
// Controls how columns are rendered and manipulated in the table view
+// Define the required helper functions
+const valueGetter = (field) => (params) => params.row[field];
+const renderCell = (params) => params.value;
+const renderDescription = (params) => params.value;
+const renderDate = (params) => params.value;
+const renderChips = (getChipProps) => (params) => params.value;
+const renderTags = (params) => params.value;
+
const columns = [
{
field: "flow_id",
@@ -144,5 +165,3 @@ FlowSearchContainer.getLayout = function getLayout(page) {
};
FlowSearchContainer.displayName = "FlowSearchContainer";
-
-export default FlowSearchContainer;
diff --git a/app/src/pages/flows.js b/app/src/pages/flows.js
new file mode 100644
index 00000000..a823b561
--- /dev/null
+++ b/app/src/pages/flows.js
@@ -0,0 +1,251 @@
+/**
+ * Flow Search Page - SEO Optimized
+ *
+ * This page displays a searchable list of flows from OpenML
+ * Route: /flows
+ *
+ * Key Concepts:
+ * 1. getStaticProps - Pre-renders this page at build time (good for SEO)
+ * 2. serverSideTranslations - Loads translations for internationalization
+ * 3. SearchContainer - Reusable component that handles all search logic
+ */
+
+import React from "react";
+import { useNextRouting } from "../utils/useNextRouting";
+import Head from "next/head";
+
+import DashboardLayout from "../layouts/Dashboard";
+import SearchContainer from "../components/search/SearchContainer";
+import {
+ renderCell,
+ valueGetter,
+ renderDescription,
+ renderDate,
+ renderTags,
+ renderChips,
+} from "../components/search/ResultTable";
+
+import flowConfig from "../search_configs/flowConfig";
+
+// Server-side translation
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+/**
+ * getStaticProps - Next.js function that runs at build time
+ *
+ * What it does:
+ * - Loads translation files for the current locale (language)
+ * - Runs on the SERVER, not in the browser
+ * - Makes the page load faster and better for SEO
+ *
+ * @param {object} context - Contains locale info (en, de, fr, etc.)
+ * @returns {object} Props to pass to the page component
+ */
+export async function getStaticProps(context) {
+ const { locale } = context; // Get current language (e.g., 'en', 'de')
+
+ return {
+ props: {
+ // Pass translations to the page
+ ...(await serverSideTranslations(locale)),
+ },
+ };
+}
+
+/**
+ * getChipProps - Defines how chips look in table view
+ *
+ * @param {string} value - The chip value
+ * @returns {object} Chip configuration (label, icon, color)
+ */
+const getChipProps = (value) => {
+ switch (value) {
+ default:
+ return {
+ label: value,
+ };
+ }
+};
+
+/**
+ * sort_options - Defines how users can sort search results
+ *
+ * Each option has:
+ * - name: Translation key for the label
+ * - value: Elasticsearch sort configuration
+ */
+const sort_options = [
+ {
+ name: "search.relevance",
+ value: [], // Default relevance sorting
+ },
+ {
+ name: "search.most_runs",
+ value: [{ field: "runs", direction: "desc" }],
+ },
+ {
+ name: "search.most_likes",
+ value: [{ field: "nr_of_likes", direction: "desc" }],
+ },
+ {
+ name: "search.most_downloads",
+ value: [{ field: "nr_of_downloads", direction: "desc" }],
+ },
+ {
+ name: "search.most_recent",
+ value: [{ field: "date", direction: "desc" }],
+ },
+];
+
+/**
+ * search_facets - Defines filter options in the sidebar
+ *
+ * Facets allow users to filter results by:
+ * - Dependencies (libraries/packages)
+ */
+const search_facets = [
+ {
+ label: "filters.dependencies",
+ field: "dependencies.keyword",
+ },
+];
+
+/**
+ * columns - Defines the table columns for flow search results
+ *
+ * Each column has:
+ * - field: The data field from Elasticsearch
+ * - headerName: Display name in the table header
+ * - valueGetter: Function to extract the value
+ * - renderCell: Function to render the cell content
+ * - width: Column width in pixels
+ */
+const columns = [
+ {
+ field: "flow_id",
+ headerName: "Flow_id",
+ valueGetter: valueGetter("flow_id"),
+ renderCell: renderCell,
+ width: 70,
+ },
+ {
+ field: "name",
+ headerName: "Name",
+ valueGetter: valueGetter("name"),
+ renderCell: renderCell,
+ width: 230,
+ },
+ {
+ field: "version",
+ headerName: "Version",
+ valueGetter: valueGetter("version"),
+ renderCell: renderCell,
+ width: 60,
+ },
+ {
+ field: "description",
+ headerName: "Description",
+ valueGetter: valueGetter("description"),
+ renderCell: renderDescription,
+ width: 360,
+ },
+ {
+ field: "date",
+ headerName: "Date",
+ valueGetter: valueGetter("date"),
+ renderCell: renderDate,
+ },
+ {
+ field: "creator",
+ headerName: "Creator",
+ valueGetter: valueGetter("creator"),
+ renderCell: renderCell,
+ width: 150,
+ },
+ {
+ field: "tags",
+ headerName: "Tags",
+ valueGetter: valueGetter("tags"),
+ renderCell: renderTags,
+ width: 400,
+ },
+];
+
+/**
+ * FlowsPage - Main component for the flows search page
+ *
+ * How it works:
+ * 1. Uses useNextRouting hook to handle URL query parameters
+ * 2. Passes configuration to SearchContainer
+ * 3. SearchContainer handles all the search logic, filters, pagination, etc.
+ *
+ * @returns {JSX.Element} The flows search page
+ */
+function FlowsPage() {
+ // useNextRouting hook synchronizes URL params with search state
+ // Example: /flows?dependencies=sklearn&sort=runs
+ const combinedConfig = useNextRouting(flowConfig, "/flows");
+
+ return (
+ <>
+ {/* SEO Meta Tags - These help Google and other search engines */}
+
+ OpenML Flows - Search Machine Learning Workflows
+
+
+
+ {/* Open Graph tags for social media sharing */}
+
+
+
+
+ {/* Canonical URL - tells search engines this is the main URL */}
+
+
+
+ {/* SearchContainer is a reusable component that handles:
+ - Elasticsearch queries
+ - Filters and facets
+ - Sorting
+ - Pagination
+ - Results display (grid, list, table views)
+ */}
+
+ >
+ );
+}
+
+/**
+ * getLayout - Wraps the page with DashboardLayout
+ *
+ * This is a Next.js pattern for persistent layouts.
+ * The DashboardLayout includes:
+ * - Navigation sidebar
+ * - Header
+ * - Footer
+ *
+ * This layout persists when navigating between pages,
+ * preventing unnecessary re-renders.
+ */
+FlowsPage.getLayout = function getLayout(page) {
+ return {page} ;
+};
+
+// Helpful for debugging in React DevTools
+FlowsPage.displayName = "FlowsPage";
+
+export default FlowsPage;
diff --git a/app/src/pages/r/search.js b/app/src/pages/r/search.js
index 1e8e46eb..b2a129b9 100644
--- a/app/src/pages/r/search.js
+++ b/app/src/pages/r/search.js
@@ -1,114 +1,27 @@
-import React from "react";
-import { useNextRouting } from "../../utils/useNextRouting";
-
-import DashboardLayout from "../../layouts/Dashboard";
-import SearchContainer from "../../components/search/SearchContainer";
-import {
- renderCell,
- valueGetter,
- renderDescription,
- renderDate,
- renderTags,
-} from "../../components/search/ResultTable";
-
-import runConfig from "../../search_configs/runConfig";
-
-// Server-side translation
-import { serverSideTranslations } from "next-i18next/serverSideTranslations";
-export async function getStaticProps(context) {
- // extract the locale identifier from the URL
- const { locale } = context;
- return {
- props: {
- // pass the translation props to the page component
- ...(await serverSideTranslations(locale)),
- },
- };
-}
-
-// Defines chips in table view
-const getChipProps = (value) => {
- switch (value) {
- default:
- return {
- label: value,
- };
- }
-};
-
-const sort_options = [
- {
- name: "search.most_recent",
- value: [{ field: "date", direction: "desc" }],
- },
- {
- name: "search.relevance",
- value: [],
- },
-];
-
-const search_facets = [
- {
- label: "filters.dataset",
- field: "run_task.source_data.name.keyword",
- },
- {
- label: "filters.tasktype",
- field: "run_task.tasktype.name.keyword",
- },
- {
- label: "filters.flow",
- field: "run_flow.name",
- },
-];
-
-// Controls how columns are rendered and manipulated in the table view
-const columns = [
- {
- field: "row_id",
- headerName: "Row_id",
- valueGetter: valueGetter("row_id"),
- renderCell: renderCell,
- width: 70,
- },
- {
- field: "description",
- headerName: "Description",
- valueGetter: valueGetter("description"),
- renderCell: renderDescription,
- width: 360,
- },
- {
- field: "date",
- headerName: "Date",
- valueGetter: valueGetter("date"),
- renderCell: renderDate,
- },
- {
- field: "tags",
- headerName: "Tags",
- valueGetter: valueGetter("tags"),
- renderCell: renderTags,
- width: 400,
- },
-];
-
-function RunSearchContainer() {
- const combinedConfig = useNextRouting(runConfig, "");
- return (
-
- );
+/**
+ * Run Search Redirect Page
+ *
+ * This page redirects from the old /r/search URL to the new SEO-friendly /runs URL
+ * Preserves all query parameters during redirect
+ */
+
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+
+export default function RunSearchRedirect() {
+ const router = useRouter();
+
+ useEffect(() => {
+ // Get all query parameters from current URL
+ const { pathname, query } = router;
+
+ // Redirect to /runs with the same query parameters
+ router.replace({
+ pathname: "/runs",
+ query: query,
+ });
+ }, [router]);
+
+ // Show nothing while redirecting
+ return null;
}
-
-RunSearchContainer.getLayout = function getLayout(page) {
- return {page} ;
-};
-
-RunSearchContainer.displayName = "RunSearchContainer";
-
-export default RunSearchContainer;
diff --git a/app/src/pages/runs.js b/app/src/pages/runs.js
new file mode 100644
index 00000000..e83e2023
--- /dev/null
+++ b/app/src/pages/runs.js
@@ -0,0 +1,227 @@
+/**
+ * Run Search Page - SEO Optimized
+ *
+ * This page displays a searchable list of runs from OpenML
+ * Route: /runs
+ *
+ * Key Concepts:
+ * 1. getStaticProps - Pre-renders this page at build time (good for SEO)
+ * 2. serverSideTranslations - Loads translations for internationalization
+ * 3. SearchContainer - Reusable component that handles all search logic
+ */
+
+import React from "react";
+import { useNextRouting } from "../utils/useNextRouting";
+import Head from "next/head";
+
+import DashboardLayout from "../layouts/Dashboard";
+import SearchContainer from "../components/search/SearchContainer";
+import {
+ renderCell,
+ valueGetter,
+ renderDescription,
+ renderDate,
+ renderTags,
+} from "../components/search/ResultTable";
+
+import runConfig from "../search_configs/runConfig";
+
+// Server-side translation
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+/**
+ * getStaticProps - Next.js function that runs at build time
+ *
+ * What it does:
+ * - Loads translation files for the current locale (language)
+ * - Runs on the SERVER, not in the browser
+ * - Makes the page load faster and better for SEO
+ *
+ * @param {object} context - Contains locale info (en, de, fr, etc.)
+ * @returns {object} Props to pass to the page component
+ */
+export async function getStaticProps(context) {
+ const { locale } = context; // Get current language (e.g., 'en', 'de')
+
+ return {
+ props: {
+ // Pass translations to the page
+ ...(await serverSideTranslations(locale)),
+ },
+ };
+}
+
+/**
+ * getChipProps - Defines how chips look in table view
+ *
+ * @param {string} value - The chip value
+ * @returns {object} Chip configuration (label, icon, color)
+ */
+const getChipProps = (value) => {
+ switch (value) {
+ default:
+ return {
+ label: value,
+ };
+ }
+};
+
+/**
+ * sort_options - Defines how users can sort search results
+ *
+ * Each option has:
+ * - name: Translation key for the label
+ * - value: Elasticsearch sort configuration
+ */
+const sort_options = [
+ {
+ name: "search.most_recent",
+ value: [{ field: "date", direction: "desc" }],
+ },
+ {
+ name: "search.relevance",
+ value: [], // Default relevance sorting
+ },
+];
+
+/**
+ * search_facets - Defines filter options in the sidebar
+ *
+ * Facets allow users to filter results by:
+ * - Dataset
+ * - Task type
+ * - Flow
+ */
+const search_facets = [
+ {
+ label: "filters.dataset",
+ field: "run_task.source_data.name.keyword",
+ },
+ {
+ label: "filters.tasktype",
+ field: "run_task.tasktype.name.keyword",
+ },
+ {
+ label: "filters.flow",
+ field: "run_flow.name",
+ },
+];
+
+/**
+ * columns - Defines the table columns for run search results
+ *
+ * Each column has:
+ * - field: The data field from Elasticsearch
+ * - headerName: Display name in the table header
+ * - valueGetter: Function to extract the value
+ * - renderCell: Function to render the cell content
+ * - width: Column width in pixels
+ */
+const columns = [
+ {
+ field: "row_id",
+ headerName: "Row_id",
+ valueGetter: valueGetter("row_id"),
+ renderCell: renderCell,
+ width: 70,
+ },
+ {
+ field: "description",
+ headerName: "Description",
+ valueGetter: valueGetter("description"),
+ renderCell: renderDescription,
+ width: 360,
+ },
+ {
+ field: "date",
+ headerName: "Date",
+ valueGetter: valueGetter("date"),
+ renderCell: renderDate,
+ },
+ {
+ field: "tags",
+ headerName: "Tags",
+ valueGetter: valueGetter("tags"),
+ renderCell: renderTags,
+ width: 400,
+ },
+];
+
+/**
+ * RunsPage - Main component for the runs search page
+ *
+ * How it works:
+ * 1. Uses useNextRouting hook to handle URL query parameters
+ * 2. Passes configuration to SearchContainer
+ * 3. SearchContainer handles all the search logic, filters, pagination, etc.
+ *
+ * @returns {JSX.Element} The runs search page
+ */
+function RunsPage() {
+ // useNextRouting hook synchronizes URL params with search state
+ // Example: /runs?tasktype=classification&sort=date
+ const combinedConfig = useNextRouting(runConfig, "/runs");
+
+ return (
+ <>
+ {/* SEO Meta Tags - These help Google and other search engines */}
+
+ OpenML Runs - Search Machine Learning Experiment Runs
+
+
+
+ {/* Open Graph tags for social media sharing */}
+
+
+
+
+ {/* Canonical URL - tells search engines this is the main URL */}
+
+
+
+ {/* SearchContainer is a reusable component that handles:
+ - Elasticsearch queries
+ - Filters and facets
+ - Sorting
+ - Pagination
+ - Results display (grid, list, table views)
+ */}
+
+ >
+ );
+}
+
+/**
+ * getLayout - Wraps the page with DashboardLayout
+ *
+ * This is a Next.js pattern for persistent layouts.
+ * The DashboardLayout includes:
+ * - Navigation sidebar
+ * - Header
+ * - Footer
+ *
+ * This layout persists when navigating between pages,
+ * preventing unnecessary re-renders.
+ */
+RunsPage.getLayout = function getLayout(page) {
+ return {page} ;
+};
+
+// Helpful for debugging in React DevTools
+RunsPage.displayName = "RunsPage";
+
+export default RunsPage;
diff --git a/app/src/pages/t/search.js b/app/src/pages/t/search.js
index 7efc14a8..45b06fce 100644
--- a/app/src/pages/t/search.js
+++ b/app/src/pages/t/search.js
@@ -1,91 +1,27 @@
-import React from "react";
-import { useNextRouting } from "../../utils/useNextRouting";
-
-import DashboardLayout from "../../layouts/Dashboard";
-import SearchContainer from "../../components/search/SearchContainer";
-import { renderCell, valueGetter } from "../../components/search/ResultTable";
-
-import taskConfig from "../../search_configs/taskConfig";
-
-// Server-side translation
-import { serverSideTranslations } from "next-i18next/serverSideTranslations";
-export async function getStaticProps(context) {
- // extract the locale identifier from the URL
- const { locale } = context;
- return {
- props: {
- // pass the translation props to the page component
- ...(await serverSideTranslations(locale)),
- },
- };
-}
-
-const sort_options = [
- {
- name: "search.relevance",
- value: [],
- },
- {
- name: "search.most_runs",
- value: [{ field: "runs", direction: "desc" }],
- },
- {
- name: "search.most_likes",
- value: [{ field: "nr_of_likes", direction: "desc" }],
- },
- {
- name: "search.most_downloads",
- value: [{ field: "nr_of_downloads", direction: "desc" }],
- },
- {
- name: "search.most_recent",
- value: [{ field: "date", direction: "desc" }],
- },
-];
-
-const search_facets = [
- {
- label: "filters.tasktype",
- field: "tasktype.name.keyword",
- },
- {
- label: "filters.estimation_procedure",
- field: "estimation_procedure.name.keyword",
- },
- {
- label: "filters.evaluation_measures",
- field: "evaluation_measures.keyword",
- },
-];
-
-// Controls how columns are rendered and manipulated in the table view
-const columns = [
- {
- field: "task_id",
- headerName: "Task_id",
- valueGetter: valueGetter("task_id"),
- renderCell: renderCell,
- width: 70,
- },
-];
-
-function TaskSearchContainer() {
- const combinedConfig = useNextRouting(taskConfig, "");
-
- return (
-
- );
+/**
+ * Task Search Redirect Page
+ *
+ * This page redirects from the old /t/search URL to the new SEO-friendly /tasks URL
+ * Preserves all query parameters during redirect
+ */
+
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+
+export default function TaskSearchRedirect() {
+ const router = useRouter();
+
+ useEffect(() => {
+ // Get all query parameters from current URL
+ const { pathname, query } = router;
+
+ // Redirect to /tasks with the same query parameters
+ router.replace({
+ pathname: "/tasks",
+ query: query,
+ });
+ }, [router]);
+
+ // Show nothing while redirecting
+ return null;
}
-
-TaskSearchContainer.getLayout = function getLayout(page) {
- return {page} ;
-};
-
-TaskSearchContainer.displayName = "TaskSearchContainer";
-
-export default TaskSearchContainer;
diff --git a/app/src/pages/tasks.js b/app/src/pages/tasks.js
new file mode 100644
index 00000000..33915c7f
--- /dev/null
+++ b/app/src/pages/tasks.js
@@ -0,0 +1,198 @@
+/**
+ * Task Search Page - SEO Optimized
+ *
+ * This page displays a searchable list of tasks from OpenML
+ * Route: /tasks
+ *
+ * Key Concepts:
+ * 1. getStaticProps - Pre-renders this page at build time (good for SEO)
+ * 2. serverSideTranslations - Loads translations for internationalization
+ * 3. SearchContainer - Reusable component that handles all search logic
+ */
+
+import React from "react";
+import { useNextRouting } from "../utils/useNextRouting";
+import Head from "next/head";
+
+import DashboardLayout from "../layouts/Dashboard";
+import SearchContainer from "../components/search/SearchContainer";
+import { renderCell, valueGetter } from "../components/search/ResultTable";
+
+import taskConfig from "../search_configs/taskConfig";
+
+// Server-side translation
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+/**
+ * getStaticProps - Next.js function that runs at build time
+ *
+ * What it does:
+ * - Loads translation files for the current locale (language)
+ * - Runs on the SERVER, not in the browser
+ * - Makes the page load faster and better for SEO
+ *
+ * @param {object} context - Contains locale info (en, de, fr, etc.)
+ * @returns {object} Props to pass to the page component
+ */
+export async function getStaticProps(context) {
+ const { locale } = context; // Get current language (e.g., 'en', 'de')
+
+ return {
+ props: {
+ // Pass translations to the page
+ ...(await serverSideTranslations(locale)),
+ },
+ };
+}
+
+/**
+ * sort_options - Defines how users can sort search results
+ *
+ * Each option has:
+ * - name: Translation key for the label
+ * - value: Elasticsearch sort configuration
+ */
+const sort_options = [
+ {
+ name: "search.relevance",
+ value: [], // Default relevance sorting
+ },
+ {
+ name: "search.most_runs",
+ value: [{ field: "runs", direction: "desc" }],
+ },
+ {
+ name: "search.most_likes",
+ value: [{ field: "nr_of_likes", direction: "desc" }],
+ },
+ {
+ name: "search.most_downloads",
+ value: [{ field: "nr_of_downloads", direction: "desc" }],
+ },
+ {
+ name: "search.most_recent",
+ value: [{ field: "date", direction: "desc" }],
+ },
+];
+
+/**
+ * search_facets - Defines filter options in the sidebar
+ *
+ * Facets allow users to filter results by:
+ * - Task type (classification, regression, etc.)
+ * - Estimation procedure (cross-validation, holdout, etc.)
+ * - Evaluation measures (accuracy, AUC, etc.)
+ */
+const search_facets = [
+ {
+ label: "filters.tasktype",
+ field: "tasktype.name.keyword",
+ },
+ {
+ label: "filters.estimation_procedure",
+ field: "estimation_procedure.name.keyword",
+ },
+ {
+ label: "filters.evaluation_measures",
+ field: "evaluation_measures.keyword",
+ },
+];
+
+/**
+ * columns - Defines the table columns for task search results
+ *
+ * Each column has:
+ * - field: The data field from Elasticsearch
+ * - headerName: Display name in the table header
+ * - valueGetter: Function to extract the value
+ * - renderCell: Function to render the cell content
+ * - width: Column width in pixels
+ */
+const columns = [
+ {
+ field: "task_id",
+ headerName: "Task_id",
+ valueGetter: valueGetter("task_id"),
+ renderCell: renderCell,
+ width: 70,
+ },
+];
+
+/**
+ * TasksPage - Main component for the tasks search page
+ *
+ * How it works:
+ * 1. Uses useNextRouting hook to handle URL query parameters
+ * 2. Passes configuration to SearchContainer
+ * 3. SearchContainer handles all the search logic, filters, pagination, etc.
+ *
+ * @returns {JSX.Element} The tasks search page
+ */
+function TasksPage() {
+ // useNextRouting hook synchronizes URL params with search state
+ // Example: /tasks?tasktype=classification&sort=runs
+ const combinedConfig = useNextRouting(taskConfig, "/tasks");
+
+ return (
+ <>
+ {/* SEO Meta Tags - These help Google and other search engines */}
+
+ OpenML Tasks - Search Machine Learning Tasks
+
+
+
+ {/* Open Graph tags for social media sharing */}
+
+
+
+
+ {/* Canonical URL - tells search engines this is the main URL */}
+
+
+
+ {/* SearchContainer is a reusable component that handles:
+ - Elasticsearch queries
+ - Filters and facets
+ - Sorting
+ - Pagination
+ - Results display (grid, list, table views)
+ */}
+
+ >
+ );
+}
+
+/**
+ * getLayout - Wraps the page with DashboardLayout
+ *
+ * This is a Next.js pattern for persistent layouts.
+ * The DashboardLayout includes:
+ * - Navigation sidebar
+ * - Header
+ * - Footer
+ *
+ * This layout persists when navigating between pages,
+ * preventing unnecessary re-renders.
+ */
+TasksPage.getLayout = function getLayout(page) {
+ return {page} ;
+};
+
+// Helpful for debugging in React DevTools
+TasksPage.displayName = "TasksPage";
+
+export default TasksPage;
diff --git a/app/src/search_configs/dataConfig.js b/app/src/search_configs/dataConfig.js
index 85ac5c99..d8f29e4e 100644
--- a/app/src/search_configs/dataConfig.js
+++ b/app/src/search_configs/dataConfig.js
@@ -1,5 +1,6 @@
-import Connector from "../services/SearchAPIConnector";
-const apiConnector = new Connector("data");
+// Using custom OpenML connector instead of API proxy
+import OpenMLSearchConnector from "../services/OpenMLSearchConnector";
+const apiConnector = new OpenMLSearchConnector("data");
const searchConfig = {
apiConnector: apiConnector,
@@ -110,16 +111,16 @@ const searchConfig = {
},
autocompleteQuery: {
search_fields: {
- name: {}
+ name: {},
},
result_fields: {
name: { snippet: { size: 100, fallback: true } },
- url: { raw: {} }
+ url: { raw: {} },
},
suggestions: {
// Optional, only if you want 'keyword' type suggestions
- }
- }
+ },
+ },
};
export default searchConfig;
diff --git a/app/src/search_configs/flowConfig.js b/app/src/search_configs/flowConfig.js
index 135ae0bf..e7abdd04 100644
--- a/app/src/search_configs/flowConfig.js
+++ b/app/src/search_configs/flowConfig.js
@@ -1,5 +1,6 @@
-import Connector from "../services/SearchAPIConnector";
-const apiConnector = new Connector("flow");
+// Using custom OpenML connector instead of API proxy
+import OpenMLSearchConnector from "../services/OpenMLSearchConnector";
+const apiConnector = new OpenMLSearchConnector("flow");
const searchConfig = {
apiConnector: apiConnector,
diff --git a/app/src/search_configs/runConfig.js b/app/src/search_configs/runConfig.js
index f1ed99e1..fae2a8ab 100644
--- a/app/src/search_configs/runConfig.js
+++ b/app/src/search_configs/runConfig.js
@@ -1,5 +1,6 @@
-import Connector from "../services/SearchAPIConnector";
-const apiConnector = new Connector("run");
+// Using custom OpenML connector instead of API proxy
+import OpenMLSearchConnector from "../services/OpenMLSearchConnector";
+const apiConnector = new OpenMLSearchConnector("run");
const searchConfig = {
apiConnector: apiConnector,
diff --git a/app/src/search_configs/taskConfig.js b/app/src/search_configs/taskConfig.js
index f01e00da..8c44297f 100644
--- a/app/src/search_configs/taskConfig.js
+++ b/app/src/search_configs/taskConfig.js
@@ -1,5 +1,6 @@
-import Connector from "../services/SearchAPIConnector";
-const apiConnector = new Connector("task");
+// Using custom OpenML connector instead of API proxy
+import OpenMLSearchConnector from "../services/OpenMLSearchConnector";
+const apiConnector = new OpenMLSearchConnector("task");
const searchConfig = {
apiConnector: apiConnector,
diff --git a/app/src/services/OpenMLSearchConnector.js b/app/src/services/OpenMLSearchConnector.js
new file mode 100644
index 00000000..094e6b0f
--- /dev/null
+++ b/app/src/services/OpenMLSearchConnector.js
@@ -0,0 +1,265 @@
+/**
+ * Custom Elasticsearch connector for OpenML
+ * Directly queries Elasticsearch without using the problematic @elastic/search-ui-elasticsearch-connector
+ */
+class OpenMLSearchConnector {
+ constructor(indexName) {
+ this.baseUrl = "https://www.openml.org/es/";
+ this.indexName = indexName;
+ }
+
+ /**
+ * Build Elasticsearch query from Search UI request state
+ */
+ buildQuery(requestState, queryConfig) {
+ const {
+ searchTerm,
+ filters,
+ current,
+ resultsPerPage,
+ sortField,
+ sortDirection,
+ sortList,
+ } = requestState;
+
+ console.log("[OpenMLSearchConnector] buildQuery - Sort info:", {
+ sortField,
+ sortDirection,
+ sortList,
+ fullRequestState: requestState,
+ });
+
+ const query = {
+ bool: {
+ must: [],
+ filter: [],
+ },
+ };
+
+ // Add search term if provided
+ if (searchTerm) {
+ query.bool.must.push({
+ multi_match: {
+ query: searchTerm,
+ fields: Object.keys(queryConfig.search_fields || {}).map(
+ (field) =>
+ `${field}^${queryConfig.search_fields[field].weight || 1}`,
+ ),
+ },
+ });
+ } else {
+ // Match all if no search term
+ query.bool.must.push({ match_all: {} });
+ }
+
+ // Add filters
+ if (filters) {
+ Object.entries(filters).forEach(([fieldName, filterValues]) => {
+ filterValues.forEach((filterValue) => {
+ if (filterValue.type === "any") {
+ // Term filter for exact matches
+ query.bool.filter.push({
+ term: { [fieldName]: filterValue.value },
+ });
+ } else if (filterValue.type === "all") {
+ // Must match all values
+ query.bool.filter.push({
+ term: { [fieldName]: filterValue.value },
+ });
+ } else if (filterValue.from || filterValue.to) {
+ // Range filter for numeric fields
+ const range = {};
+ if (filterValue.from) range.gte = filterValue.from;
+ if (filterValue.to) range.lte = filterValue.to;
+ query.bool.filter.push({
+ range: { [fieldName]: range },
+ });
+ }
+ });
+ });
+ }
+
+ // Build aggregations for facets
+ const aggs = {};
+ if (queryConfig.facets) {
+ Object.entries(queryConfig.facets).forEach(([fieldName, facetConfig]) => {
+ if (facetConfig.type === "value") {
+ aggs[fieldName] = {
+ terms: {
+ field: fieldName,
+ size: facetConfig.size || 10,
+ },
+ };
+ } else if (facetConfig.type === "range") {
+ // Strip out 'name' field from ranges - ES doesn't accept it
+ const esRanges = facetConfig.ranges.map((range) => {
+ const { name, ...esRange } = range;
+ return esRange;
+ });
+ aggs[fieldName] = {
+ range: {
+ field: fieldName,
+ ranges: esRanges,
+ },
+ };
+ }
+ });
+ }
+
+ const from = (current - 1) * resultsPerPage;
+ const size = resultsPerPage;
+
+ const esQuery = {
+ query,
+ from,
+ size,
+ aggs,
+ };
+
+ // Add sorting - Search UI uses sortList array format
+ if (sortList && sortList.length > 0) {
+ esQuery.sort = sortList.map((sortItem) => ({
+ [sortItem.field]: { order: sortItem.direction },
+ }));
+ } else if (sortField && sortDirection) {
+ // Fallback for direct sortField/sortDirection
+ esQuery.sort = [{ [sortField]: { order: sortDirection } }];
+ }
+
+ return esQuery;
+ }
+
+ /**
+ * Format Elasticsearch response to Search UI format
+ * Search UI expects fields in { raw: value } format
+ */
+ formatResponse(esResponse, requestState) {
+ const { hits, aggregations } = esResponse;
+
+ const results = hits.hits.map((hit) => {
+ const formattedResult = {
+ id: { raw: hit._id },
+ _meta: {
+ id: hit._id,
+ score: hit._score,
+ },
+ };
+
+ // Wrap all source fields in { raw: value } format
+ Object.entries(hit._source).forEach(([key, value]) => {
+ formattedResult[key] = { raw: value };
+ });
+
+ return formattedResult;
+ });
+
+ const totalResults = hits.total.value || hits.total;
+
+ const facets = {};
+ if (aggregations) {
+ Object.entries(aggregations).forEach(([fieldName, agg]) => {
+ if (agg.buckets) {
+ facets[fieldName] = [
+ {
+ type: "value",
+ data: agg.buckets.map((bucket) => ({
+ value: bucket.key,
+ count: bucket.doc_count,
+ })),
+ },
+ ];
+ }
+ });
+ }
+
+ return {
+ results,
+ totalResults,
+ facets,
+ requestId: Date.now().toString(),
+ };
+ }
+
+ /**
+ * Main search method called by Search UI
+ */
+ async onSearch(requestState, queryConfig) {
+ console.log("[OpenMLSearchConnector] onSearch called", {
+ requestState,
+ queryConfig,
+ });
+
+ try {
+ const esQuery = this.buildQuery(requestState, queryConfig);
+ console.log(
+ "[OpenMLSearchConnector] ES Query:",
+ JSON.stringify(esQuery, null, 2),
+ );
+
+ const url = `${this.baseUrl}${this.indexName}/_search`;
+ console.log("[OpenMLSearchConnector] Fetching:", url);
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(esQuery),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ console.error("[OpenMLSearchConnector] ES Error Response:", data);
+ throw new Error(
+ `ES responded with ${response.status}: ${response.statusText}. Error: ${JSON.stringify(data)}`,
+ );
+ }
+ console.log("[OpenMLSearchConnector] ES Response:", {
+ totalHits: data.hits?.total,
+ resultsCount: data.hits?.hits?.length,
+ });
+
+ const formattedResponse = this.formatResponse(data, requestState);
+ console.log("[OpenMLSearchConnector] Formatted response:", {
+ totalResults: formattedResponse.totalResults,
+ resultsCount: formattedResponse.results.length,
+ facetCount: Object.keys(formattedResponse.facets).length,
+ });
+
+ return formattedResponse;
+ } catch (error) {
+ console.error("[OpenMLSearchConnector] Error:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Autocomplete method (optional, called by Search UI for suggestions)
+ */
+ async onAutocomplete(requestState, queryConfig) {
+ console.log("[OpenMLSearchConnector] onAutocomplete called", {
+ requestState,
+ });
+ // Return empty results for now
+ return {
+ results: [],
+ };
+ }
+
+ /**
+ * Result click tracking (optional)
+ */
+ onResultClick() {
+ // No-op for now
+ }
+
+ /**
+ * Autocomplete result click tracking (optional)
+ */
+ onAutocompleteResultClick() {
+ // No-op for now
+ }
+}
+
+export default OpenMLSearchConnector;
diff --git a/diagnose.sh b/diagnose.sh
new file mode 100755
index 00000000..c2d45171
--- /dev/null
+++ b/diagnose.sh
@@ -0,0 +1,115 @@
+#!/bin/bash
+
+# π Quick Diagnostic Script
+# Helps identify the cause of 500 errors
+
+echo "π OpenML Elasticsearch Diagnostic"
+echo "==================================="
+echo ""
+
+# Colors
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+# Test 1: Check if Next.js is running
+echo "π Test 1: Next.js Server"
+if curl -s http://localhost:3001 > /dev/null 2>&1; then
+ echo -e "${GREEN}β
Next.js is running on port 3001${NC}"
+else
+ echo -e "${RED}β Next.js is NOT running${NC}"
+ echo " Run: cd app && npm run dev"
+ exit 1
+fi
+echo ""
+
+# Test 2: Check Elasticsearch connection
+echo "π Test 2: Elasticsearch Connection"
+if curl -s https://www.openml.org/es/ > /dev/null 2>&1; then
+ echo -e "${GREEN}β
Elasticsearch is reachable${NC}"
+ ES_INFO=$(curl -s https://www.openml.org/es/)
+ echo " Cluster: $(echo $ES_INFO | grep -o '"cluster_name":"[^"]*"' | cut -d'"' -f4)"
+ echo " Version: $(echo $ES_INFO | grep -o '"number":"[^"]*"' | head -1 | cut -d'"' -f4)"
+else
+ echo -e "${RED}β Cannot reach Elasticsearch${NC}"
+ echo " URL: https://www.openml.org/es/"
+ echo " Check internet connection or VPN"
+fi
+echo ""
+
+# Test 3: Check data index
+echo "π Test 3: Data Index Query"
+ES_SEARCH=$(curl -s -X POST "https://www.openml.org/es/data/_search" \
+ -H "Content-Type: application/json" \
+ -d '{"size":1,"query":{"match_all":{}}}')
+
+if echo "$ES_SEARCH" | grep -q '"hits"'; then
+ TOTAL=$(echo $ES_SEARCH | grep -o '"total":[0-9]*' | head -1 | grep -o '[0-9]*')
+ if [ -z "$TOTAL" ]; then
+ TOTAL=$(echo $ES_SEARCH | grep -o '"value":[0-9]*' | head -1 | grep -o '[0-9]*')
+ fi
+ echo -e "${GREEN}β
Can query 'data' index${NC}"
+ echo " Total datasets: $TOTAL"
+else
+ echo -e "${RED}β Cannot query 'data' index${NC}"
+ echo " Response: $(echo $ES_SEARCH | head -c 100)..."
+fi
+echo ""
+
+# Test 4: Check Next.js test endpoint
+echo "π Test 4: Next.js Test Endpoint"
+TEST_RESULT=$(curl -s http://localhost:3001/api/test-es)
+
+if echo "$TEST_RESULT" | grep -q '"success":true'; then
+ echo -e "${GREEN}β
Test endpoint working${NC}"
+ echo " Visit: http://localhost:3001/api/test-es"
+else
+ echo -e "${RED}β Test endpoint failed${NC}"
+ if echo "$TEST_RESULT" | grep -q '"error"'; then
+ ERROR_MSG=$(echo $TEST_RESULT | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
+ echo " Error: $ERROR_MSG"
+ fi
+fi
+echo ""
+
+# Test 5: Check package installation
+echo "π Test 5: Required Packages"
+cd app > /dev/null 2>&1
+if npm list @elastic/search-ui-elasticsearch-connector > /dev/null 2>&1; then
+ echo -e "${GREEN}β
Elasticsearch connector installed${NC}"
+else
+ echo -e "${RED}β Elasticsearch connector missing${NC}"
+ echo " Run: npm install @elastic/search-ui-elasticsearch-connector"
+fi
+
+if npm list @elastic/react-search-ui > /dev/null 2>&1; then
+ echo -e "${GREEN}β
React Search UI installed${NC}"
+else
+ echo -e "${RED}β React Search UI missing${NC}"
+ echo " Run: npm install @elastic/react-search-ui"
+fi
+cd - > /dev/null 2>&1
+echo ""
+
+# Summary
+echo "==================================="
+echo "π― Summary & Next Steps"
+echo "==================================="
+echo ""
+echo "1. Open your browser to:"
+echo -e " ${BLUE}http://localhost:3001/api/test-es${NC}"
+echo ""
+echo "2. Check the Next.js terminal output for error messages"
+echo ""
+echo "3. Open browser console (F12) and check for errors"
+echo ""
+echo "4. If all tests pass but still getting 500 error:"
+echo " - Clear browser cache (Ctrl+Shift+Delete)"
+echo " - Kill Next.js (Ctrl+C) and restart: npm run dev"
+echo " - Check app/src/search_configs/dataConfig.js"
+echo ""
+echo "5. Read detailed guide:"
+echo -e " ${BLUE}cat ERROR_ANALYSIS.md${NC}"
+echo ""
diff --git a/server/src/client/app/package-lock.json b/server/src/client/app/package-lock.json
index 42d9729d..6e206f03 100644
--- a/server/src/client/app/package-lock.json
+++ b/server/src/client/app/package-lock.json
@@ -127,6 +127,7 @@
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@@ -782,6 +783,7 @@
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz",
"integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
},
@@ -1591,6 +1593,7 @@
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz",
"integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==",
+ "peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-module-imports": "^7.22.15",
@@ -2394,6 +2397,7 @@
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
"integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
@@ -2434,6 +2438,7 @@
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
"integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
@@ -2629,6 +2634,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
"hasInstallScript": true,
+ "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.36"
},
@@ -3663,6 +3669,7 @@
"version": "5.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.20.tgz",
"integrity": "sha512-CS2pUuqxST7ch9VNDCklRYDbJ3rru20Tx7na92QvVVKfu3RL4z/QLuVIc8jYGsdCnauMaeUSlFNLAJNb0yXe6w==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.1",
"@floating-ui/react-dom": "^2.0.2",
@@ -3786,6 +3793,7 @@
"version": "5.14.14",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.14.tgz",
"integrity": "sha512-cAmCwAHFQXxb44kWbVFkhKATN8tACgMsFwrXo8ro6WzYW73U/qsR5AcCiJIhCyYYg+gcftfkmNcpRaV3JjhHCg==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.1",
"@mui/base": "5.0.0-beta.20",
@@ -3969,6 +3977,7 @@
"version": "5.14.14",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.14.tgz",
"integrity": "sha512-y4InFmCgGGWXnz+iK4jRTWVikY0HgYnABjz4wgiUgEa2W1H8M4ow+27BegExUWPkj4TWthQ2qG9FOGSMtI+PKA==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.1",
"@mui/private-theming": "^5.14.14",
@@ -5277,6 +5286,7 @@
"version": "17.0.68",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.68.tgz",
"integrity": "sha512-y8heXejd/Gi43S28GOqIFmr6BzhLa3anMlPojRu4rHh3MtRrrpB+BtLEcqP3XPO1urXByzBdkOLU7sodYWnpkA==",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -5475,6 +5485,7 @@
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -5865,6 +5876,7 @@
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5956,6 +5968,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -7003,6 +7016,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
@@ -7894,6 +7908,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -9010,6 +9025,7 @@
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -9405,6 +9421,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -11379,6 +11396,7 @@
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12268,6 +12286,7 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
+ "peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@@ -15668,6 +15687,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -16906,6 +16926,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@@ -18013,6 +18034,7 @@
"version": "6.0.13",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -18241,6 +18263,7 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -18419,6 +18442,7 @@
"version": "0.29.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz",
"integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==",
+ "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
@@ -18537,6 +18561,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
@@ -18728,6 +18753,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -18829,7 +18855,8 @@
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
- "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "peer": true
},
"node_modules/react-markdown": {
"version": "8.0.7",
@@ -18878,6 +18905,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18906,6 +18934,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
@@ -18940,6 +18969,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
"integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==",
+ "peer": true,
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@@ -19161,6 +19191,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -19613,6 +19644,7 @@
"version": "2.79.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -20666,6 +20698,7 @@
"version": "5.3.11",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
"integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
+ "peer": true,
"dependencies": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5",
@@ -21579,6 +21612,7 @@
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -21679,6 +21713,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -22233,6 +22268,7 @@
"version": "5.89.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
"integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.0",
@@ -22301,6 +22337,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -22350,6 +22387,7 @@
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
"integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
+ "peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -22408,6 +22446,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -22782,6 +22821,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
diff --git a/test-datasets-route.sh b/test-datasets-route.sh
new file mode 100755
index 00000000..8a6ec7fa
--- /dev/null
+++ b/test-datasets-route.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+# π§ͺ Test Script for /datasets Route
+# This script helps you verify that the new route is working
+
+echo "π§ͺ Testing OpenML Next.js Routes"
+echo "=================================="
+echo ""
+
+# Colors for output
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Check if Next.js is running
+echo "π Step 1: Checking if Next.js is running..."
+if curl -s http://localhost:3001 > /dev/null; then
+ echo -e "${GREEN}β
Next.js is running on port 3001${NC}"
+else
+ echo -e "${RED}β Next.js is NOT running on port 3001${NC}"
+ echo " Please run: cd app && npm run dev"
+ exit 1
+fi
+
+echo ""
+echo "π Step 2: Testing new /datasets route..."
+HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/datasets)
+if [ $HTTP_CODE -eq 200 ]; then
+ echo -e "${GREEN}β
/datasets route returns 200 OK${NC}"
+else
+ echo -e "${RED}β /datasets route returns $HTTP_CODE${NC}"
+ echo " Check the browser console for errors"
+fi
+
+echo ""
+echo "π Step 3: Testing redirect from /d/search..."
+HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/d/search)
+if [ $HTTP_CODE -eq 200 ]; then
+ echo -e "${GREEN}β
/d/search route returns 200 OK${NC}"
+ echo " (Should redirect to /datasets in browser)"
+else
+ echo -e "${RED}β /d/search route returns $HTTP_CODE${NC}"
+fi
+
+echo ""
+echo "π Step 4: Testing API endpoint..."
+HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/api/search)
+if [ $HTTP_CODE -eq 400 ]; then
+ echo -e "${YELLOW}β οΈ /api/search returns 400 (expected without data)${NC}"
+ echo " This is normal - it needs POST data"
+else
+ echo -e "${YELLOW}β οΈ /api/search returns $HTTP_CODE${NC}"
+fi
+
+echo ""
+echo "=================================="
+echo "π― Manual Testing Steps:"
+echo "=================================="
+echo ""
+echo "1. Open browser: http://localhost:3001/datasets"
+echo " β Should see the search page"
+echo " β Should see filters on the left"
+echo " β Should see search results"
+echo ""
+echo "2. Try searching for 'iris'"
+echo " β Results should filter"
+echo ""
+echo "3. Open: http://localhost:3001/d/search"
+echo " β Should redirect to /datasets"
+echo " β URL should change in browser"
+echo ""
+echo "4. Open browser console (F12 β Console)"
+echo " β Look for any red errors"
+echo ""
+echo "=================================="
+echo "π If you see errors:"
+echo "=================================="
+echo ""
+echo "1. Check Elasticsearch connection:"
+echo " curl https://www.openml.org/es/"
+echo ""
+echo "2. Check browser console (F12)"
+echo ""
+echo "3. Check Next.js terminal output"
+echo ""
+echo "4. Read the troubleshooting guide:"
+echo " See NEXTJS_LEARNING_GUIDE.md"
+echo ""
+
diff --git a/validation.md b/validation.md
new file mode 100644
index 00000000..c2e32a11
--- /dev/null
+++ b/validation.md
@@ -0,0 +1,314 @@
+**Current Server-Side Validation in Flask + React vs Next.js Client-Side Validation**
+
+## Current Flask Server-Side Validations:
+
+Based on my analysis, your Flask backend has minimal formal validation. Here's what's currently implemented:
+
+## User Authentication (views.py & views.py):
+
+Email existence check - checks if user already exists during signup
+Password verification - uses Argon2/Bcrypt for password hashing
+User activation status - checks if user account is confirmed
+Token validation - JWT tokens for authentication
+
+## Data Upload (views.py):
+
+File format validation - only checks file extension:
+ββ>
+supported_extensions = [".csv", ".parquet", ".json", ".feather", ".arff"]
+if file_extension not in supported_extensions:
+return jsonify({"msg": "format not supported"})
+<ββ
+
+1. Empty string to None conversion - basic data sanitization
+2. Ownership verification - checks if user is dataset owner before allowing edits
+3. NO file size limits
+4. NO content structure validation
+5. NO malicious file scanning
+
+## Image Upload (views.py):
+
+1. Filename sanitization using secure_filename()
+2. NO file type validation
+3. NO file size limits
+4. NO image format verification
+
+What's Missing (Security Concerns):
+β No file size limits (DoS vulnerability)
+β No MIME type validation
+β No content inspection
+β No rate limiting
+β No input sanitization for XSS
+β No SQL injection protection (using raw queries)
+β No CSV/JSON structure validation
+β No schema validation libraries (no Marshmallow, Pydantic, WTForms)
+
+## React Client-Side Validation - You're Correct! β
+
+## React itself does NOT provide built-in validation. However, your current React app uses:
+
+## Formik + Yup (found in SignUp.js and SignIn.js):
+
+ββ>
+validationSchema={Yup.object().shape({
+firstName: Yup.string().max(255).required("First name is required"),
+email: Yup.string()
+.email("Must be a valid email")
+.max(255)
+.required("Email is required"),
+password: Yup.string()
+.min(12, "Must be at least 12 characters")
+.max(255)
+.required("Required"),
+})}
+<ββ
+
+## Current validation in React forms:
+
+β
Email format validation
+β
Password length (min 12 chars)
+β
Required field checks
+β
Password confirmation matching
+However, DataUpload.js has NO client-side validation:
+
+β No file size checks
+β No file type validation before upload
+β No form field validation
+Only uses react-dropzone with maxSize: 1000000000 (1GB) but doesn't enforce it properly
+
+βββββββββββββββββ +++++++++++**\*\*\*\*** +++++++++++++++ βββββββββββββββββββ
+
+## Next.js Client-Side Validation Strategy:
+
+For your Next.js migration, here are the best practices for client-side validation:
+
+1. Form Validation Libraries:
+ Option A: React Hook Form + Zod (Modern, TypeScript-first)
+ ββ>
+ import { useForm } from 'react-hook-form';
+ import { zodResolver } from '@hookform/resolvers/zod';
+ import \* as z from 'zod';
+
+const datasetSchema = z.object({
+dataset_name: z.string().min(3).max(255),
+description: z.string().min(10).max(5000),
+creator: z.string().optional(),
+file: z.custom()
+.refine((file) => file.size <= 100_000_000, 'Max file size is 100MB')
+.refine(
+(file) => ['text/csv', 'application/json', 'text/plain'].includes(file.type),
+'Only CSV, JSON, and ARFF files are allowed'
+),
+});
+<ββ
+
+Option B: Formik + Yup (What you're currently using)
+ββ>
+validationSchema={Yup.object().shape({
+dataset_name: Yup.string().required().max(255),
+description: Yup.string().required().min(10).max(5000),
+file: Yup.mixed()
+.required('A file is required')
+.test('fileSize', 'File too large', (value) => {
+return value && value.size <= 100_000_000;
+})
+.test('fileType', 'Unsupported file format', (value) => {
+return value && ['text/csv', 'application/json'].includes(value.type);
+}),
+})}
+<ββ
+
+2. File Upload Validation:
+ Create a comprehensive validation utility:
+ ββ>
+ // lib/fileValidation.ts
+ export const ALLOWED_FILE_TYPES = {
+ 'text/csv': ['.csv'],
+ 'application/json': ['.json'],
+ 'text/plain': ['.arff'],
+ 'application/x-feather': ['.feather'],
+ 'application/vnd.apache.parquet': ['.parquet'],
+ };
+
+export const MAX*FILE_SIZE = 100 * 1024 \_ 1024; // 100MB
+
+export function validateFile(file: File) {
+const errors: string[] = [];
+
+// Check file size
+if (file.size > MAX_FILE_SIZE) {
+errors.push(`File size must be less than ${MAX_FILE_SIZE / 1024 / 1024}MB`);
+}
+
+// Check file type
+const isValidType = Object.keys(ALLOWED_FILE_TYPES).some(mimeType =>
+file.type === mimeType
+);
+
+const isValidExtension = Object.values(ALLOWED_FILE_TYPES)
+.flat()
+.some(ext => file.name.toLowerCase().endsWith(ext));
+
+if (!isValidType && !isValidExtension) {
+errors.push('Invalid file type. Only CSV, JSON, ARFF, Feather, and Parquet files are allowed');
+}
+
+// Check file name
+if (file.name.length > 255) {
+errors.push('File name too long');
+}
+
+return {
+isValid: errors.length === 0,
+errors,
+};
+}
+
+// Advanced: Read first few bytes to verify file signature
+export async function verifyFileSignature(file: File): Promise {
+return new Promise((resolve) => {
+const reader = new FileReader();
+reader.onload = (e) => {
+const arr = new Uint8Array(e.target?.result as ArrayBuffer).subarray(0, 4);
+let header = '';
+for (let i = 0; i < arr.length; i++) {
+header += arr[i].toString(16);
+}
+// Add signature verification logic here
+resolve(true);
+};
+reader.readAsArrayBuffer(file.slice(0, 4));
+});
+}
+<ββ
+
+3. CSV Structure Validation (Client-Side):
+ ββ>
+ // lib/csvValidation.ts
+ import Papa from 'papaparse';
+
+export async function validateCSVStructure(file: File) {
+return new Promise((resolve, reject) => {
+Papa.parse(file, {
+preview: 10, // Only parse first 10 rows
+complete: (results) => {
+const errors: string[] = [];
+
+ // Check if file is empty
+ if (results.data.length === 0) {
+ errors.push('CSV file is empty');
+ }
+
+ // Check for headers
+ if (results.data.length > 0) {
+ const headers = results.data[0];
+ if (headers.length === 0) {
+ errors.push('No headers found in CSV');
+ }
+ }
+
+ // Check for consistent column count
+ const columnCounts = results.data.map(row => row.length);
+ const uniqueCounts = [...new Set(columnCounts)];
+ if (uniqueCounts.length > 1) {
+ errors.push('Inconsistent number of columns in CSV');
+ }
+
+ resolve({
+ isValid: errors.length === 0,
+ errors,
+ preview: results.data,
+ });
+ },
+ error: (error) => {
+ reject(error);
+ },
+ });
+
+});
+}
+<ββ
+
+4. Example Next.js Implementation:
+ ββ>
+ // app/upload/page.tsx
+ 'use client';
+
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import \* as z from 'zod';
+import { validateFile, validateCSVStructure } from '@/lib/fileValidation';
+
+const schema = z.object({
+datasetName: z.string().min(3).max(255),
+description: z.string().min(10).max(5000),
+creator: z.string().optional(),
+file: z.custom(),
+});
+
+export default function DataUploadPage() {
+const [fileErrors, setFileErrors] = useState([]);
+const { register, handleSubmit, formState: { errors } } = useForm({
+resolver: zodResolver(schema),
+});
+
+const onFileChange = async (e: React.ChangeEvent) => {
+const file = e.target.files?.[0];
+if (!file) return;
+
+ // Basic validation
+ const validation = validateFile(file);
+ if (!validation.isValid) {
+ setFileErrors(validation.errors);
+ return;
+ }
+
+ // CSV structure validation
+ if (file.name.endsWith('.csv')) {
+ try {
+ const csvValidation = await validateCSVStructure(file);
+ if (!csvValidation.isValid) {
+ setFileErrors(csvValidation.errors);
+ return;
+ }
+ } catch (error) {
+ setFileErrors(['Failed to parse CSV file']);
+ return;
+ }
+ }
+
+ setFileErrors([]);
+
+};
+
+const onSubmit = async (data: any) => {
+// Submit to API
+};
+
+return (
+
+
+);
+}
+<ββ
+
+## Learning Resources:
+
+1. React Hook Form + Zod:
+ Docs: https://react-hook-form.com/
+ Zod: https://zod.dev/
+
+2. Formik + Yup:
+ Formik: https://formik.org/
+ Yup: https://github.com/jquense/yup
+
+3. File validation:
+ PapaParse (CSV): https://www.papaparse.com/
+ File type detection: https://github.com/sindresorhus/file-type
+
+4. Next.js forms:
+ Server Actions: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
+ Would you like me to create a complete working example with all these validations integrated?