in packages/graph-explorer/src/modules/NodesStyling/NodeStyleDialog.tsx [80:259]
function Content({ vertexType }: { vertexType: string }) {
const t = useTranslations();
const [nodePreferences, setNodePreferences] = useAtom(
userStylingNodeAtom(vertexType)
);
const displayConfig = useDisplayVertexTypeConfig(vertexType);
const selectOptions = useMemo(() => {
const options = displayConfig.attributes.map(attr => ({
label: attr.displayLabel,
value: attr.name,
}));
options.unshift({
label: t("nodes-styling.node-type"),
value: RESERVED_TYPES_PROPERTY,
});
options.unshift({
label: t("nodes-styling.node-id"),
value: RESERVED_ID_PROPERTY,
});
return options;
}, [displayConfig.attributes, t]);
const onUserPrefsChange = useCallback(
(prefs: Omit<VertexPreferences, "type">) => {
setNodePreferences({ type: vertexType, ...prefs });
},
[setNodePreferences, vertexType]
);
const reset = useResetAtom(userStylingNodeAtom(vertexType));
const { enqueueNotification } = useNotification();
const convertImageToBase64AndSetNewIcon = useCallback(
async (file: File) => {
if (file.size > 50 * 1024) {
enqueueNotification({
title: "Invalid file",
message: "File size too large. Maximum 50Kb",
type: "error",
});
return;
}
try {
const result = await file2Base64(file);
onUserPrefsChange({ iconUrl: result, iconImageType: file.type });
} catch (error) {
console.error("Unable to convert uploaded image to base64: ", error);
}
},
[enqueueNotification, onUserPrefsChange]
);
return (
<div className="modal-container">
<div>
<p>Display Attributes</p>
<div className="attrs-container">
<SelectField
label="Display Name Attribute"
labelPlacement="inner"
value={displayConfig.displayNameAttribute}
onValueChange={value => {
onUserPrefsChange({ displayNameAttribute: value });
}}
options={selectOptions}
/>
<SelectField
label="Display Description Attribute"
labelPlacement="inner"
value={displayConfig.displayDescriptionAttribute}
onValueChange={value => {
onUserPrefsChange({
longDisplayNameAttribute: value,
});
}}
options={selectOptions}
/>
</div>
</div>
<div>
<p>Shape and Icon</p>
<div className="flex flex-row items-center gap-2">
<SelectField
label="Style"
labelPlacement="inner"
value={nodePreferences?.shape || "ellipse"}
onValueChange={value =>
onUserPrefsChange({ shape: value as ShapeStyle })
}
options={NODE_SHAPE}
className="grow"
/>
<FileButton
accept="image/*"
onChange={file => {
file && convertImageToBase64AndSetNewIcon(file);
}}
>
{props => (
<IconButton
variant="filled"
className="text-text-primary hover:text-text-primary group rounded-full border-0 bg-transparent p-0 hover:cursor-pointer hover:bg-gray-200"
icon={
<>
<div className="hidden group-hover:flex">
<UploadIcon />
</div>
<VertexSymbol
vertexStyle={displayConfig.style}
className="size-full group-hover:hidden"
/>
</>
}
tooltipText="Upload New Icon"
onClick={props.onClick}
/>
)}
</FileButton>
</div>
</div>
<div>
<p>Shape Styling</p>
<div className="attrs-container">
<ColorInput
label="Color"
labelPlacement="inner"
startColor={nodePreferences?.color || "#17457b"}
onChange={(color: string) => onUserPrefsChange({ color })}
/>
<InputField
label="Background Opacity"
labelPlacement="inner"
type="number"
min={0}
max={1}
step={0.1}
value={nodePreferences?.backgroundOpacity ?? 0.4}
onChange={(value: number) =>
onUserPrefsChange({ backgroundOpacity: value })
}
/>
</div>
</div>
<div>
<div className="attrs-container">
<ColorInput
label="Border Color"
labelPlacement="inner"
startColor={nodePreferences?.borderColor || "#17457b"}
onChange={(color: string) =>
onUserPrefsChange({ borderColor: color })
}
/>
<InputField
label="Border Width"
labelPlacement="inner"
type="number"
min={0}
value={nodePreferences?.borderWidth ?? 0}
onChange={(value: number) =>
onUserPrefsChange({ borderWidth: value })
}
/>
<SelectField
label="Border Style"
labelPlacement="inner"
value={nodePreferences?.borderStyle || "solid"}
onValueChange={value =>
onUserPrefsChange({ borderStyle: value as LineStyle })
}
options={LINE_STYLE_OPTIONS}
/>
</div>
</div>
<div className="actions">
<Button onPress={() => reset()}>Reset to Default</Button>