-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow configuration of column order (#4118)
In the lookout UI, allow the user to change the order of the columns of the jobs table through a drag-and-drop interface. This change replaces the existing column selector with a new column configuration dialog which extends the current functionality of the column selector with the ability to drag and drop columns into the user's desired order. The existing functionality which has been preserved is: - Hiding and showing columns (via a checkbox) - Adding annotation key columns - Removing and editing annotation key columns which have been added - Indicates when columns are for annotation keys The new column configuration dialog has the following additional functionalities: - Hides columns which are configured to be pinned (i.e. the selector column) - Hides columns which are grouped, and explains this to the user - When the input to add an annotation column is not visible on the screen, it displays a chip indicating its presence. Clicking on this chip will scroll to the input - Validates annotation key columns on input, making sure they have no leading or trailing whitespace, and that there is not already a column for the same annotation key This change also makes the buttons in the job action bar more visually consistent - using primary colours for actions and secondary colours for configuration changes.
- Loading branch information
1 parent
e611e13
commit 6e5c1eb
Showing
17 changed files
with
1,216 additions
and
434 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
...ookout/ui/src/components/lookoutV2/ColumnConfigurationDialog/AddAnnotationColumnInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { useState } from "react" | ||
|
||
import { AddCircle } from "@mui/icons-material" | ||
import { FormControl, FormHelperText, IconButton, InputAdornment, InputLabel, OutlinedInput } from "@mui/material" | ||
|
||
const INPUT_LABEL_TEXT = "Annotation key" | ||
const INPUT_ID = "annotation-key" | ||
|
||
export interface AddAnnotationColumnInputProps { | ||
onCreate: (annotationKey: string) => void | ||
existingAnnotationColumnKeysSet: Set<string> | ||
} | ||
|
||
export const AddAnnotationColumnInput = ({ | ||
onCreate, | ||
existingAnnotationColumnKeysSet, | ||
}: AddAnnotationColumnInputProps) => { | ||
const [value, setValue] = useState("") | ||
|
||
const isValueFilled = value.length !== 0 | ||
|
||
const valueHasNoLeadingTrailingWhitespace = value.trim() === value | ||
const valueIsNew = !existingAnnotationColumnKeysSet.has(value) | ||
const isValueValid = [valueHasNoLeadingTrailingWhitespace, valueIsNew].every(Boolean) | ||
|
||
const handleCreate = () => { | ||
onCreate(value) | ||
setValue("") | ||
} | ||
|
||
return ( | ||
<FormControl fullWidth size="small" margin="normal" error={!isValueValid}> | ||
<InputLabel htmlFor={INPUT_ID}>{INPUT_LABEL_TEXT}</InputLabel> | ||
<OutlinedInput | ||
id={INPUT_ID} | ||
label={INPUT_LABEL_TEXT} | ||
value={value} | ||
onChange={({ target }) => { | ||
setValue(target.value) | ||
}} | ||
onKeyDown={(event) => { | ||
if (event.key === "Enter" && isValueFilled && isValueValid) { | ||
handleCreate() | ||
event.currentTarget.blur() | ||
event.preventDefault() | ||
} | ||
}} | ||
endAdornment={ | ||
isValueFilled && isValueValid ? ( | ||
<InputAdornment position="end"> | ||
<IconButton | ||
aria-label={`Add a column for annotation '${value}'`} | ||
edge="end" | ||
onClick={handleCreate} | ||
title={`Add a column for annotation '${value}'`} | ||
> | ||
<AddCircle /> | ||
</IconButton> | ||
</InputAdornment> | ||
) : undefined | ||
} | ||
/> | ||
{!valueHasNoLeadingTrailingWhitespace && ( | ||
<FormHelperText>The annotation key must not have leading or trailing whitespace.</FormHelperText> | ||
)} | ||
{!valueIsNew && <FormHelperText>A column for this annotation key already exists.</FormHelperText>} | ||
</FormControl> | ||
) | ||
} |
69 changes: 69 additions & 0 deletions
69
...okout/ui/src/components/lookoutV2/ColumnConfigurationDialog/EditAnnotationColumnInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { useState } from "react" | ||
|
||
import { Check } from "@mui/icons-material" | ||
import { FormControl, FormHelperText, IconButton, InputAdornment, InputLabel, OutlinedInput } from "@mui/material" | ||
|
||
const INPUT_LABEL_TEXT = "Annotation key" | ||
const INPUT_ID = "annotation-key" | ||
|
||
export interface EditAnnotationColumnInputProps { | ||
onEdit: (annotationKey: string) => void | ||
currentAnnotationKey: string | ||
existingAnnotationColumnKeys: Set<string> | ||
} | ||
|
||
export const EditAnnotationColumnInput = ({ | ||
onEdit, | ||
existingAnnotationColumnKeys, | ||
currentAnnotationKey, | ||
}: EditAnnotationColumnInputProps) => { | ||
const [value, setValue] = useState(currentAnnotationKey) | ||
|
||
const isValueFilled = value.length !== 0 | ||
|
||
const valueHasNoLeadingTrailingWhitespace = value.trim() === value | ||
const valueIsUnique = !existingAnnotationColumnKeys.has(value) || value === currentAnnotationKey | ||
const isValueValid = [valueHasNoLeadingTrailingWhitespace, valueIsUnique].every(Boolean) | ||
|
||
const handleEdit = () => { | ||
onEdit(value) | ||
} | ||
|
||
return ( | ||
<FormControl fullWidth size="small" margin="normal" error={!isValueValid}> | ||
<InputLabel htmlFor={INPUT_ID}>{INPUT_LABEL_TEXT}</InputLabel> | ||
<OutlinedInput | ||
id={INPUT_ID} | ||
label={INPUT_LABEL_TEXT} | ||
value={value} | ||
onChange={({ target }) => { | ||
setValue(target.value) | ||
}} | ||
onKeyDown={(event) => { | ||
event.stopPropagation() | ||
if (event.key === "Enter" && isValueFilled && isValueValid) { | ||
handleEdit() | ||
} | ||
}} | ||
endAdornment={ | ||
isValueFilled && isValueValid ? ( | ||
<InputAdornment position="end"> | ||
<IconButton | ||
aria-label={`Change this column's annotation to '${value}'`} | ||
edge="end" | ||
onClick={handleEdit} | ||
title={`Change this column's annotation to '${value}'`} | ||
> | ||
<Check /> | ||
</IconButton> | ||
</InputAdornment> | ||
) : undefined | ||
} | ||
/> | ||
{!valueHasNoLeadingTrailingWhitespace && ( | ||
<FormHelperText>The annotation key must not have leading or trailing whitespace.</FormHelperText> | ||
)} | ||
{!valueIsUnique && <FormHelperText>A column for this annotation key already exists.</FormHelperText>} | ||
</FormControl> | ||
) | ||
} |
130 changes: 130 additions & 0 deletions
130
...lookout/ui/src/components/lookoutV2/ColumnConfigurationDialog/OrderableColumnListItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { useState } from "react" | ||
|
||
import { useSortable } from "@dnd-kit/sortable" | ||
import { CSS } from "@dnd-kit/utilities" | ||
import { Cancel, Delete, DragHandle, Edit } from "@mui/icons-material" | ||
import { | ||
Checkbox, | ||
IconButton, | ||
ListItem, | ||
ListItemButton, | ||
ListItemIcon, | ||
ListItemText, | ||
Stack, | ||
styled, | ||
} from "@mui/material" | ||
|
||
import { EditAnnotationColumnInput } from "./EditAnnotationColumnInput" | ||
import { SPACING } from "../../../styling/spacing" | ||
import { | ||
AnnotationColumnId, | ||
fromAnnotationColId, | ||
getColumnMetadata, | ||
JobTableColumn, | ||
toColId, | ||
} from "../../../utils/jobsTableColumns" | ||
|
||
const GrabListItemIcon = styled(ListItemIcon)({ | ||
cursor: "grab", | ||
touchAction: "none", | ||
}) | ||
|
||
const EditAnnotationColumnInputContainer = styled(Stack)({ | ||
width: "100%", | ||
}) | ||
|
||
export interface OrderableColumnListItemProps { | ||
column: JobTableColumn | ||
isVisible: boolean | ||
onToggleVisibility: () => void | ||
removeAnnotationColumn: () => void | ||
editAnnotationColumn: (annotationKey: string) => void | ||
existingAnnotationColumnKeysSet: Set<string> | ||
} | ||
|
||
export const OrderableColumnListItem = ({ | ||
column, | ||
isVisible, | ||
onToggleVisibility, | ||
removeAnnotationColumn, | ||
editAnnotationColumn, | ||
existingAnnotationColumnKeysSet, | ||
}: OrderableColumnListItemProps) => { | ||
const colId = toColId(column.id) | ||
const colMetadata = getColumnMetadata(column) | ||
const colIsAnnotation = colMetadata.annotation ?? false | ||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ | ||
id: colId, | ||
}) | ||
|
||
const [isEditing, setIsEditing] = useState(false) | ||
|
||
return ( | ||
<ListItem | ||
key={colId} | ||
disablePadding | ||
dense | ||
style={{ transform: CSS.Transform.toString(transform), transition }} | ||
secondaryAction={ | ||
colIsAnnotation && !isEditing ? ( | ||
<> | ||
<IconButton | ||
data-no-dnd | ||
title="Edit column" | ||
onClick={() => { | ||
setIsEditing(true) | ||
}} | ||
> | ||
<Edit /> | ||
</IconButton> | ||
<IconButton data-no-dnd edge="end" title="Delete column" onClick={removeAnnotationColumn}> | ||
<Delete /> | ||
</IconButton> | ||
</> | ||
) : undefined | ||
} | ||
ref={setNodeRef} | ||
{...attributes} | ||
{...listeners} | ||
> | ||
<GrabListItemIcon> | ||
<DragHandle /> | ||
</GrabListItemIcon> | ||
{colIsAnnotation && isEditing ? ( | ||
<EditAnnotationColumnInputContainer data-no-dnd spacing={SPACING.xs} direction="row"> | ||
<EditAnnotationColumnInput | ||
onEdit={(annotationKey) => { | ||
editAnnotationColumn(annotationKey) | ||
setIsEditing(false) | ||
}} | ||
currentAnnotationKey={fromAnnotationColId(colId as AnnotationColumnId)} | ||
existingAnnotationColumnKeys={existingAnnotationColumnKeysSet} | ||
/> | ||
<div> | ||
<IconButton onClick={() => setIsEditing(false)} title="Cancel changes"> | ||
<Cancel /> | ||
</IconButton> | ||
</div> | ||
</EditAnnotationColumnInputContainer> | ||
) : ( | ||
<ListItemButton onClick={onToggleVisibility} dense disabled={!column.enableHiding} tabIndex={2} data-no-dnd> | ||
<ListItemIcon> | ||
<Checkbox | ||
edge="start" | ||
checked={isVisible} | ||
tabIndex={-1} | ||
disableRipple | ||
inputProps={{ "aria-labelledby": colId }} | ||
size="small" | ||
/> | ||
</ListItemIcon> | ||
<ListItemText | ||
id={colId} | ||
primary={colMetadata.displayName} | ||
secondary={colIsAnnotation ? "Annotation" : undefined} | ||
/> | ||
</ListItemButton> | ||
)} | ||
</ListItem> | ||
) | ||
} |
Oops, something went wrong.