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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* under the License.
*/

import { useMemo } from "react";
import {
useGetBuild,
useGetBuildLogs,
Expand Down Expand Up @@ -78,6 +79,10 @@ export function BuildLogs({
build?.status
);

const reversedBuildLogs = useMemo(
() => (buildLogs ? [...buildLogs].reverse() : undefined),
[buildLogs]
);
const getEmptyStateMessage = () => {
if (error) {
return {
Expand Down Expand Up @@ -143,7 +148,7 @@ export function BuildLogs({
{isBuildLoading && <InfoLoadingSkeleton />}
{build && <BuildSteps build={build} />}
<LogsPanel
logs={buildLogs}
logs={reversedBuildLogs}
isLoading={isLoading}
error={error}
showSearch={false}
Expand Down
13 changes: 13 additions & 0 deletions console/workspaces/pages/eval/src/CreateMonitor.Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
useCreateMonitor,
useListEnvironments,
useListMonitors,
} from "@agent-management-platform/api-client";
import { type CreateMonitorFormValues } from "./form/schema";
import { MonitorFormWizard } from "./subComponents/MonitorFormWizard";
Expand All @@ -42,6 +43,12 @@ export const CreateMonitorComponent: React.FC = () => {

const envId = data && data.length > 0 ? data[0].name : undefined;

const { data: monitorsData } = useListMonitors({
orgName: orgId ?? "",
projName: projectId ?? "",
agentName: agentId ?? "",
});

const {
mutate: createMonitor,
isPending,
Expand Down Expand Up @@ -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 (
<MonitorFormWizard
title="Create Monitor"
Expand All @@ -133,6 +145,7 @@ export const CreateMonitorComponent: React.FC = () => {
isSubmitting={isPending}
serverError={error}
missingParamsMessage={missingParamsMessage}
existingMonitorNames={existingMonitorNames}
/>
);
};
Expand Down
14 changes: 14 additions & 0 deletions console/workspaces/pages/eval/src/EditMonitor.Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -216,6 +228,8 @@ export const EditMonitorComponent: React.FC = () => {
isSubmitting={isUpdating}
serverError={updateError}
isTypeEditable={false}
existingMonitorNames={existingMonitorNames}
editingMonitorName={monitorData?.name}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const EvalMonitorsComponent: React.FC = () => {
envId: envId,
},
)}
endIcon={<Plus />}
startIcon={<Plus />}
color="primary"
>
Add monitor
Expand Down
6 changes: 3 additions & 3 deletions console/workspaces/pages/eval/src/form/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function CreateMonitorForm({
<Form.Stack>
<Form.Section>
<Form.Header>Basic Details</Form.Header>
<Form.ElementWrapper name="displayName" label="Monitor Title">
<Form.ElementWrapper name="displayName" label="Name">
<TextField
id="displayName"
placeholder="Enter monitor name"
Expand All @@ -69,7 +69,7 @@ export function CreateMonitorForm({
}
error={!!errors.displayName}
helperText={
errors.displayName ?? "Visible label shown in the monitors list"
errors.displayName ?? ""
}
/>
</Form.ElementWrapper>
Expand Down Expand Up @@ -195,8 +195,7 @@ export function CreateMonitorForm({
}
error={!!errors.intervalMinutes}
helperText={
errors.intervalMinutes ??
"How often the monitor should execute"
errors.intervalMinutes
}
fullWidth
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ export function EvaluatorLlmProviderSection({
select
size="small"
fullWidth
value={draftProviderName}
value={draftProviderName ? draftProviderName : "_empty"}
onChange={(e) => handleProviderChange(e.target.value)}
>
<MenuItem value="">Select a provider</MenuItem>
<MenuItem value="_empty" disabled>Select a provider</MenuItem>
{availableProvidersToAdd.map((p) => (
<MenuItem key={p.name} value={p.name}>
{p.displayName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type MetricsTooltipEntry = {
value?: number;
color?: string;
dataKey?: string;
payload?: Record<string, unknown>;
};

interface MetricsTooltipProps {
Expand All @@ -41,10 +42,17 @@ const MetricsTooltip: React.FC<MetricsTooltipProps> = ({
return null;
}

const label = payload[0]?.payload?.xLabel as string | undefined;

return (
<Card variant="outlined">
<CardContent>
<Stack direction="column" gap={0.5}>
{label && (
<Typography variant="caption" color="text.secondary" fontWeight={600}>
{label}
</Typography>
)}
{payload.map((entry) => (
<Stack
key={entry.dataKey ?? entry.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ interface MonitorFormWizardProps {
missingParamsMessage?: string | null;
backLabel?: string;
isTypeEditable?: boolean;
existingMonitorNames?: string[];
editingMonitorName?: string;
}

export function MonitorFormWizard({
Expand All @@ -56,6 +58,8 @@ export function MonitorFormWizard({
missingParamsMessage,
backLabel = "Back to Monitors",
isTypeEditable = true,
existingMonitorNames = [],
editingMonitorName,
}: MonitorFormWizardProps) {
const [page, setPage] = useState<1 | 2>(1);
const [formData, setFormData] =
Expand Down Expand Up @@ -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.",
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate-check here compares next.name (the auto-generated slug) against existingMonitorNames (also slugs from MonitorResponse.name). But the field being validated is displayName, not the slug.

This means:

  • A displayName rename that happens to produce a colliding slug will be incorrectly blocked
  • Two monitors with identical displayName but different slugs will pass validation (false negative)

Should compare normalized displayName values end-to-end instead. Callers (CreateMonitor.Component.tsx, EditMonitor.Component.tsx) should pass m.displayName and monitorData?.displayName.

Same issue applies to the submit-time check at lines ~212-223.

}

return next;
});
},
[setFieldError, validateField],
[setFieldError, validateField, existingMonitorNames, editingMonitorName],
);

const handleToggleEvaluator = useCallback(
Expand Down Expand Up @@ -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;
Expand Down
48 changes: 24 additions & 24 deletions console/workspaces/pages/eval/src/subComponents/MonitorRunList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ import {
useTheme,
CircularProgress,
IconButton,
Button,
} from "@wso2/oxygen-ui";
import {
Activity,
AlertTriangle,
CheckCircle,
CircleAlert,
RefreshCcw,
Repeat,
Play,
} from "@wso2/oxygen-ui-icons-react";
import {
useListMonitorRuns,
Expand Down Expand Up @@ -333,29 +334,28 @@ export default function MonitorRunList() {
align="center"
onClick={(e) => e.stopPropagation()}
>
<Tooltip title="Re-run">
<span>
<IconButton
size="small"
disabled={
isRerunning ||
run.status === "running" ||
run.status === "pending"
}
onClick={() =>
rerunMonitor({
monitorName: monitorId ?? "",
orgName: orgId ?? "",
projName: projectId ?? "",
agentName: agentId ?? "",
runId: run.id,
})
}
>
<Repeat size={16} />
</IconButton>
</span>
</Tooltip>
<Button
startIcon={<Play size={16} />}
variant="outlined"
color="secondary"
size="small"
disabled={
isRerunning ||
run.status === "running" ||
run.status === "pending"
}
onClick={() =>
rerunMonitor({
monitorName: monitorId ?? "",
orgName: orgId ?? "",
projName: projectId ?? "",
agentName: agentId ?? "",
runId: run.id,
})
}
>
Re-run
</Button>
</ListingTable.Cell>
</ListingTable.Row>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -98,13 +98,7 @@ export function MonitorStartStopButton({

if (isPastMonitor) {
return (
<Tooltip title="Past monitors cannot be started">
<span>
<IconButton disabled>
<Calendar size={16} />
</IconButton>
</span>
</Tooltip>
null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Since isPastMonitor now triggers an early return null, the isPastMonitor branch in the tooltipTitle ternary (around line 60) is unreachable dead code. Should simplify to:

const tooltipTitle = isActive ? "Stop Monitor" : "Start Monitor";

Also, the return ( null ); can be simplified to just return null;.

);
}
if (isStarting || isStopping) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export function MonitorTable() {
<ListingTable.Cell align="center">Status</ListingTable.Cell>
<ListingTable.Cell>Data Source</ListingTable.Cell>
<ListingTable.Cell>Evaluators</ListingTable.Cell>
<ListingTable.Cell>Actions</ListingTable.Cell>
<ListingTable.Cell align="right">Actions</ListingTable.Cell>
</ListingTable.Row>
</ListingTable.Head>
<ListingTable.Body>
Expand Down Expand Up @@ -285,7 +285,7 @@ export function MonitorTable() {
</Stack>
</ListingTable.Cell>
<ListingTable.Cell onClick={(e) => e.stopPropagation()}>
<Stack direction="row" spacing={1} alignItems="center">
<Stack direction="row" spacing={1} justifyContent="end">
<MonitorStartStopButton
monitorName={monitor.name}
monitorType={monitor.type}
Expand Down
Loading