Skip to content
Closed
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
@@ -0,0 +1,97 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
import { ReactElement } from 'react';

export interface ActionConfirmationDialogProps {
/**
* Whether the dialog is open
*/
open: boolean;

/**
* Title of the confirmation dialog
*/
title?: string;

/**
* Message displayed in the dialog body
*/
message?: string;

/**
* Label for the confirm button
*/
actionLabel?: string;

/**
* Number of selected items (displayed in the message if provided)
*/
selectedItemCount?: number;

/**
* Callback when user confirms the action
*/
onConfirm: () => void;

/**
* Callback when user cancels the action
*/
onCancel: () => void;

/**
* Whether the action is currently being executed (shows loading state)
*/
isLoading?: boolean;
}

/**
* Reusable confirmation dialog for destructive or important selection actions
*/
export function ActionConfirmationDialog({
open,
title = 'Confirm Action',
message,
actionLabel = 'Confirm',
selectedItemCount,
onConfirm,
onCancel,
isLoading = false,
}: ActionConfirmationDialogProps): ReactElement {
const defaultMessage = selectedItemCount
? `Are you sure you want to perform this action on ${selectedItemCount} selected item${selectedItemCount > 1 ? 's' : ''}?`
: 'Are you sure you want to perform this action?';

return (
<Dialog
open={open}
onClose={onCancel}
aria-labelledby="action-confirmation-dialog-title"
aria-describedby="action-confirmation-dialog-description"
>
<DialogTitle id="action-confirmation-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="action-confirmation-dialog-description">{message || defaultMessage}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
<Button onClick={onConfirm} color="primary" variant="contained" disabled={isLoading}>
{isLoading ? 'Executing...' : actionLabel}
</Button>
</DialogActions>
</Dialog>
);
}
14 changes: 14 additions & 0 deletions components/src/ActionConfirmationDialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export * from './ActionConfirmationDialog';
234 changes: 234 additions & 0 deletions components/src/SelectionActions/ActionConditionEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {
Box,
FormControl,
FormControlLabel,
InputLabel,
MenuItem,
Select,
Stack,
Switch,
TextField,
Typography,
} from '@mui/material';
import { ReactElement } from 'react';
import { ActionCondition } from './selection-action-model';

export interface ActionConditionEditorProps {
condition?: ActionCondition;
onChange: (condition: ActionCondition | undefined) => void;
availableColumns?: string[];
}

type ConditionKind = 'Value' | 'Range' | 'Regex' | 'Misc';

/**
* Editor for configuring action visibility conditions
*/
export function ActionConditionEditor({
condition,
onChange,
availableColumns = [],
}: ActionConditionEditorProps): ReactElement {
const hasCondition = condition !== undefined;

function handleToggleCondition(enabled: boolean): void {
if (enabled) {
onChange({
kind: 'Value',
spec: { value: '' },
});
} else {
onChange(undefined);
}
}

function handleKindChange(kind: ConditionKind): void {
switch (kind) {
case 'Value':
onChange({ kind: 'Value', spec: { value: '' } });
break;
case 'Range':
onChange({ kind: 'Range', spec: {} });
break;
case 'Regex':
onChange({ kind: 'Regex', spec: { expr: '' } });
break;
case 'Misc':
onChange({ kind: 'Misc', spec: { value: 'empty' } });
break;
}
}

return (
<Stack spacing={1}>
<FormControlLabel
control={
<Switch checked={hasCondition} onChange={(e) => handleToggleCondition(e.target.checked)} size="small" />
}
label="Only show action when condition matches"
/>

{hasCondition && condition && (
<Box sx={{ pl: 2, borderLeft: 2, borderColor: 'divider' }}>
<Stack spacing={2}>
<FormControl size="small" sx={{ minWidth: 150 }}>
<InputLabel>Condition Type</InputLabel>
<Select
value={condition.kind}
label="Condition Type"
onChange={(e) => handleKindChange(e.target.value as ConditionKind)}
>
<MenuItem value="Value">Exact Value</MenuItem>
<MenuItem value="Range">Number Range</MenuItem>
<MenuItem value="Regex">Regex Match</MenuItem>
<MenuItem value="Misc">Special Values</MenuItem>
</Select>
</FormControl>

{condition.kind === 'Value' && (
<ValueConditionEditor
value={condition.spec.value}
onChange={(value) => onChange({ kind: 'Value', spec: { value } })}
/>
)}

{condition.kind === 'Range' && (
<RangeConditionEditor
min={condition.spec.min}
max={condition.spec.max}
onChange={(min, max) => onChange({ kind: 'Range', spec: { min, max } })}
/>
)}

{condition.kind === 'Regex' && (
<RegexConditionEditor
expr={condition.spec.expr}
onChange={(expr) => onChange({ kind: 'Regex', spec: { expr } })}
/>
)}

{condition.kind === 'Misc' && (
<MiscConditionEditor
value={condition.spec.value}
onChange={(value) => onChange({ kind: 'Misc', spec: { value } })}
/>
)}

{availableColumns.length > 0 && (
<Typography variant="caption" color="text.secondary">
Condition is evaluated against the values in selected rows. Action is shown if ANY selected row matches.
</Typography>
)}
</Stack>
</Box>
)}

{!hasCondition && (
<Typography variant="body2" color="text.secondary">
Action will always be visible when rows are selected.
</Typography>
)}
</Stack>
);
}

interface ValueConditionEditorProps {
value: string;
onChange: (value: string) => void;
}

function ValueConditionEditor({ value, onChange }: ValueConditionEditorProps): ReactElement {
return (
<TextField
label="Match Value"
value={value}
onChange={(e) => onChange(e.target.value)}
size="small"
helperText="Row must contain this exact value in any column"
/>
);
}

interface RangeConditionEditorProps {
min?: number;
max?: number;
onChange: (min?: number, max?: number) => void;
}

function RangeConditionEditor({ min, max, onChange }: RangeConditionEditorProps): ReactElement {
return (
<Stack direction="row" spacing={2}>
<TextField
label="Min Value"
type="number"
value={min ?? ''}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined, max)}
size="small"
sx={{ flex: 1 }}
/>
<TextField
label="Max Value"
type="number"
value={max ?? ''}
onChange={(e) => onChange(min, e.target.value ? Number(e.target.value) : undefined)}
size="small"
sx={{ flex: 1 }}
/>
</Stack>
);
}

interface RegexConditionEditorProps {
expr: string;
onChange: (expr: string) => void;
}

function RegexConditionEditor({ expr, onChange }: RegexConditionEditorProps): ReactElement {
return (
<TextField
label="Regex Pattern"
value={expr}
onChange={(e) => onChange(e.target.value)}
size="small"
helperText="Row value must match this regular expression"
placeholder="^(active|running)$"
/>
);
}

interface MiscConditionEditorProps {
value: 'empty' | 'null' | 'NaN' | 'true' | 'false';
onChange: (value: 'empty' | 'null' | 'NaN' | 'true' | 'false') => void;
}

function MiscConditionEditor({ value, onChange }: MiscConditionEditorProps): ReactElement {
return (
<FormControl size="small" sx={{ minWidth: 150 }}>
<InputLabel>Special Value</InputLabel>
<Select
value={value}
label="Special Value"
onChange={(e) => onChange(e.target.value as MiscConditionEditorProps['value'])}
>
<MenuItem value="empty">Empty</MenuItem>
<MenuItem value="null">Null</MenuItem>
<MenuItem value="NaN">NaN</MenuItem>
<MenuItem value="true">True</MenuItem>
<MenuItem value="false">False</MenuItem>
</Select>
</FormControl>
);
}
Loading