diff --git a/console/workspaces/libs/shared-component/src/components/BuildLogs.tsx b/console/workspaces/libs/shared-component/src/components/BuildLogs.tsx index 08e4103ff..985f3414d 100644 --- a/console/workspaces/libs/shared-component/src/components/BuildLogs.tsx +++ b/console/workspaces/libs/shared-component/src/components/BuildLogs.tsx @@ -16,6 +16,7 @@ * under the License. */ +import { useMemo } from "react"; import { useGetBuild, useGetBuildLogs, @@ -78,6 +79,10 @@ export function BuildLogs({ build?.status ); + const reversedBuildLogs = useMemo( + () => (buildLogs ? [...buildLogs].reverse() : undefined), + [buildLogs] + ); const getEmptyStateMessage = () => { if (error) { return { @@ -143,7 +148,7 @@ export function BuildLogs({ {isBuildLoading && } {build && } { const envId = data && data.length > 0 ? data[0].name : undefined; + const { data: monitorsData } = useListMonitors({ + orgName: orgId ?? "", + projName: projectId ?? "", + agentName: agentId ?? "", + }); + const { mutate: createMonitor, isPending, @@ -123,6 +130,11 @@ export const CreateMonitorComponent: React.FC = () => { [agentId, backHref, createMonitor, envId, navigate, orgId, projectId], ); + const existingMonitorNames = useMemo( + () => monitorsData?.monitors?.map((m) => m.name) ?? [], + [monitorsData], + ); + return ( { isSubmitting={isPending} serverError={error} missingParamsMessage={missingParamsMessage} + existingMonitorNames={existingMonitorNames} /> ); }; diff --git a/console/workspaces/pages/eval/src/EditMonitor.Component.tsx b/console/workspaces/pages/eval/src/EditMonitor.Component.tsx index 5c7cce0c4..2fdee4372 100644 --- a/console/workspaces/pages/eval/src/EditMonitor.Component.tsx +++ b/console/workspaces/pages/eval/src/EditMonitor.Component.tsx @@ -26,6 +26,7 @@ import { } from "@agent-management-platform/types"; import { useGetMonitor, + useListMonitors, useUpdateMonitor, } from "@agent-management-platform/api-client"; import { MonitorFormWizard } from "./subComponents/MonitorFormWizard"; @@ -40,6 +41,12 @@ export const EditMonitorComponent: React.FC = () => { }>(); const navigate = useNavigate(); + const { data: monitorsData } = useListMonitors({ + orgName: orgId ?? "", + projName: projectId ?? "", + agentName: agentId ?? "", + }); + const { data: monitorData, isLoading, @@ -114,6 +121,11 @@ export const EditMonitorComponent: React.FC = () => { }; }, [monitorData]); + const existingMonitorNames = useMemo( + () => monitorsData?.monitors?.map((m) => m.name) ?? [], + [monitorsData], + ); + const handleUpdateMonitor = useCallback( (values: CreateMonitorFormValues) => { if (!orgId || !projectId || !agentId || !monitorId) { @@ -216,6 +228,8 @@ export const EditMonitorComponent: React.FC = () => { isSubmitting={isUpdating} serverError={updateError} isTypeEditable={false} + existingMonitorNames={existingMonitorNames} + editingMonitorName={monitorData?.name} /> ); }; diff --git a/console/workspaces/pages/eval/src/EvalMonitors.Component.tsx b/console/workspaces/pages/eval/src/EvalMonitors.Component.tsx index aacc48439..9a184f360 100644 --- a/console/workspaces/pages/eval/src/EvalMonitors.Component.tsx +++ b/console/workspaces/pages/eval/src/EvalMonitors.Component.tsx @@ -50,7 +50,7 @@ export const EvalMonitorsComponent: React.FC = () => { envId: envId, }, )} - endIcon={} + startIcon={} color="primary" > Add monitor diff --git a/console/workspaces/pages/eval/src/form/schema.ts b/console/workspaces/pages/eval/src/form/schema.ts index 520fb1f82..606b8f243 100644 --- a/console/workspaces/pages/eval/src/form/schema.ts +++ b/console/workspaces/pages/eval/src/form/schema.ts @@ -42,9 +42,9 @@ export const createMonitorSchema = z displayName: z .string() .trim() - .min(1, "Monitor title is required") - .min(3, "Monitor title must be at least 3 characters") - .max(120, "Monitor title must be at most 120 characters"), + .min(1, "Name is required") + .min(3, "Name must be at least 3 characters") + .max(120, "Name must be at most 120 characters"), name: z .string() .trim() diff --git a/console/workspaces/pages/eval/src/subComponents/CreateMonitorForm.tsx b/console/workspaces/pages/eval/src/subComponents/CreateMonitorForm.tsx index 1cfa51f1e..273e2e12c 100644 --- a/console/workspaces/pages/eval/src/subComponents/CreateMonitorForm.tsx +++ b/console/workspaces/pages/eval/src/subComponents/CreateMonitorForm.tsx @@ -57,7 +57,7 @@ export function CreateMonitorForm({ Basic Details - + @@ -195,8 +195,7 @@ export function CreateMonitorForm({ } error={!!errors.intervalMinutes} helperText={ - errors.intervalMinutes ?? - "How often the monitor should execute" + errors.intervalMinutes } fullWidth /> diff --git a/console/workspaces/pages/eval/src/subComponents/EvaluatorLlmProviderSection.tsx b/console/workspaces/pages/eval/src/subComponents/EvaluatorLlmProviderSection.tsx index 11966abfc..955247b83 100644 --- a/console/workspaces/pages/eval/src/subComponents/EvaluatorLlmProviderSection.tsx +++ b/console/workspaces/pages/eval/src/subComponents/EvaluatorLlmProviderSection.tsx @@ -125,10 +125,10 @@ export function EvaluatorLlmProviderSection({ select size="small" fullWidth - value={draftProviderName} + value={draftProviderName ? draftProviderName : "_empty"} onChange={(e) => handleProviderChange(e.target.value)} > - Select a provider + Select a provider {availableProvidersToAdd.map((p) => ( {p.displayName} diff --git a/console/workspaces/pages/eval/src/subComponents/MetricsTooltip.tsx b/console/workspaces/pages/eval/src/subComponents/MetricsTooltip.tsx index e98429630..61416eeeb 100644 --- a/console/workspaces/pages/eval/src/subComponents/MetricsTooltip.tsx +++ b/console/workspaces/pages/eval/src/subComponents/MetricsTooltip.tsx @@ -24,6 +24,7 @@ type MetricsTooltipEntry = { value?: number; color?: string; dataKey?: string; + payload?: Record; }; interface MetricsTooltipProps { @@ -41,10 +42,17 @@ const MetricsTooltip: React.FC = ({ return null; } + const label = payload[0]?.payload?.xLabel as string | undefined; + return ( + {label && ( + + {label} + + )} {payload.map((entry) => ( (1); const [formData, setFormData] = @@ -100,12 +104,26 @@ export function MonitorFormWizard({ if (field === "displayName" || field === "name") { const slugError = validateField("name", next.name, next); setFieldError("name", slugError); + + // Duplicate name check: exclude current monitor when editing + const namesToCheck = existingMonitorNames.filter( + (n) => n !== editingMonitorName, + ); + const isDuplicate = + next.name.trim().length >= 3 && + namesToCheck.includes(next.name.trim()); + if (isDuplicate) { + setFieldError( + "displayName", + "A monitor with this name already exists.", + ); + } } return next; }); }, - [setFieldError, validateField], + [setFieldError, validateField, existingMonitorNames, editingMonitorName], ); const handleToggleEvaluator = useCallback( @@ -191,8 +209,29 @@ export function MonitorFormWizard({ return; } + // Duplicate name check on submit (exclude current monitor when editing) + const namesToCheck = existingMonitorNames.filter( + (n) => n !== editingMonitorName, + ); + if ( + formData.name.trim().length >= 3 && + namesToCheck.includes(formData.name.trim()) + ) { + setFieldError("displayName", "A monitor with this name already exists."); + setPage(1); + return; + } + onSubmit(formData); - }, [formData, missingParamsMessage, onSubmit, guardSubmit]); + }, [ + formData, + missingParamsMessage, + onSubmit, + guardSubmit, + existingMonitorNames, + editingMonitorName, + setFieldError, + ]); const canAdvance = formData.displayName.trim().length >= 3 && formData.name.trim().length >= 3; diff --git a/console/workspaces/pages/eval/src/subComponents/MonitorRunList.tsx b/console/workspaces/pages/eval/src/subComponents/MonitorRunList.tsx index bc57d654f..ef8db23ef 100644 --- a/console/workspaces/pages/eval/src/subComponents/MonitorRunList.tsx +++ b/console/workspaces/pages/eval/src/subComponents/MonitorRunList.tsx @@ -30,6 +30,7 @@ import { useTheme, CircularProgress, IconButton, + Button, } from "@wso2/oxygen-ui"; import { Activity, @@ -37,7 +38,7 @@ import { CheckCircle, CircleAlert, RefreshCcw, - Repeat, + Play, } from "@wso2/oxygen-ui-icons-react"; import { useListMonitorRuns, @@ -333,29 +334,28 @@ export default function MonitorRunList() { align="center" onClick={(e) => e.stopPropagation()} > - - - - rerunMonitor({ - monitorName: monitorId ?? "", - orgName: orgId ?? "", - projName: projectId ?? "", - agentName: agentId ?? "", - runId: run.id, - }) - } - > - - - - + ); diff --git a/console/workspaces/pages/eval/src/subComponents/MonitorStartStopButton.tsx b/console/workspaces/pages/eval/src/subComponents/MonitorStartStopButton.tsx index ab20a8d71..a2d378296 100644 --- a/console/workspaces/pages/eval/src/subComponents/MonitorStartStopButton.tsx +++ b/console/workspaces/pages/eval/src/subComponents/MonitorStartStopButton.tsx @@ -18,7 +18,7 @@ import { useCallback, type MouseEvent } from "react"; import { CircularProgress, IconButton, Tooltip } from "@wso2/oxygen-ui"; -import { Calendar, Pause, Play } from "@wso2/oxygen-ui-icons-react"; +import { Pause, Play } from "@wso2/oxygen-ui-icons-react"; import { useStartMonitor, useStopMonitor, @@ -98,13 +98,7 @@ export function MonitorStartStopButton({ if (isPastMonitor) { return ( - - - - - - - + null ); } if (isStarting || isStopping) { diff --git a/console/workspaces/pages/eval/src/subComponents/MonitorTable.tsx b/console/workspaces/pages/eval/src/subComponents/MonitorTable.tsx index 53322a769..9c6db4fb9 100644 --- a/console/workspaces/pages/eval/src/subComponents/MonitorTable.tsx +++ b/console/workspaces/pages/eval/src/subComponents/MonitorTable.tsx @@ -229,7 +229,7 @@ export function MonitorTable() { Status Data Source Evaluators - Actions + Actions @@ -285,7 +285,7 @@ export function MonitorTable() { e.stopPropagation()}> - + = ({ ), ).sort(); + const fullTimestampFormat = "MMM d, yyyy HH:mm:ss"; + return allTimestamps.map((ts) => { const date = new Date(ts); - const label = date.toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); - const row: Record = { xLabel: label }; + const xTimestamp = date.getTime(); + const xLabel = format(date, fullTimestampFormat); + const row: Record = { + xTimestamp, + xLabel, + }; evaluatorNames.forEach((name) => { const pt = seriesMap[name]?.find((p) => p.timestamp === ts); if (pt !== undefined && pt.mean !== null) row[name] = pt.mean; @@ -172,7 +173,8 @@ const PerformanceByEvaluatorCard: React.FC = ({ name, stroke: LINE_COLOURS[i % LINE_COLOURS.length], strokeWidth: 2, - dot: false, + type: "linear" as const, + dot: true, })), [evaluatorNames], ); @@ -184,6 +186,13 @@ const PerformanceByEvaluatorCard: React.FC = ({ const hasData = chartData.length > 0; + const formatTickTimestamp = (value: number) => { + const granularity = timeSeriesByEvaluator?.granularity; + if (granularity === "trace") { + return format(new Date(value), "hh:mm:ss a"); + } + return format(new Date(value), "MMM d, yyyy hh:mm a"); + }; return ( @@ -247,12 +256,27 @@ const PerformanceByEvaluatorCard: React.FC = ({ + `${v.toFixed(1)}%`} />