Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

better icons support in operators #4373

Merged
merged 4 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions app/packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@textea/json-viewer": "^3.4.1",
"classnames": "^2.3.1",
"framer-motion": "^6.2.7",
"material-icons": "^1.13.12",
"path-to-regexp": "^6.2.0",
"react-input-autosize": "^3.0.0",
"react-laag": "^2.0.3",
Expand Down
13 changes: 13 additions & 0 deletions app/packages/components/src/components/MuiIconFont/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Icon, IconProps } from "@mui/material";
import "material-icons/iconfont/material-icons.css";
import React from "react";

// Available Icons: https://github.com/marella/material-icons?tab=readme-ov-file#available-icons
export default function MuiIconFont(props: MuiIconFontProps) {
const { name, ...iconProps } = props;
return <Icon {...iconProps}>{name}</Icon>;
}

type MuiIconFontProps = IconProps & {
name: string;
};
1 change: 1 addition & 0 deletions app/packages/components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export { default as TabOption } from "./TabOption";
export { default as TextField } from "./TextField";
export { default as ThemeProvider, useFont, useTheme } from "./ThemeProvider";
export { default as Tooltip } from "./Tooltip";
export { default as MuiIconFont } from "./MuiIconFont";
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { MuiIconFont, PillButton } from "@fiftyone/components";
import usePanelEvent from "@fiftyone/operators/src/usePanelEvent";
import { usePanelId } from "@fiftyone/spaces";
import { IconButton, Link } from "@mui/material";
import React, { useCallback } from "react";
import styled from "styled-components";
import { getComponentProps } from "../utils";

export default function IconButtonView(props) {
const { schema } = props;
const { view = {} } = schema;
const { icon, operator, params = {}, prompt, variant, label } = view;
const panelId = usePanelId();
const handleClick = usePanelEvent();

const Icon = icon ? (
<MuiIconFont
name={icon}
sx={
variant === "round"
? {}
: { color: (theme) => theme.palette.primary.main }
}
{...getComponentProps(props, "icon")}
/>
) : null;

const onClick = useCallback(() => {
handleClick(panelId, { params, operator, prompt });
Copy link
Contributor

Choose a reason for hiding this comment

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

🆒

}, [panelId, params, operator, prompt, handleClick]);

if (variant === "round") {
return (
<PillButton
icon={Icon || label}
title={label}
highlight
onClick={onClick}
aria-label={`Button for ${icon}`}
/>
);
}

if (variant === "square") {
return (
<SquareButton
title={label}
aria-label={`Button for ${icon}`}
onClick={onClick}
{...getComponentProps(props, "button")}
>
{Icon || label}
</SquareButton>
);
}

return (
<IconButton
title={label}
aria-label={`Button for ${icon}`}
size="small"
{...getComponentProps(props, "button")}
onClick={onClick}
>
{Icon || label}
</IconButton>
);
}

const SquareButton = styled(Link)`
display: flex;
color: var(--fo-palette-primary-plainColor);
align-items: center;
cursor: pointer;
border-bottom: 1px var(--fo-palette-primary-plainColor) solid;
background: var(--fo-palette-neutral-softBg);
border-top-left-radius: 3px;
border-top-right-radius: 3px;
padding: 0.25rem;
`;
1 change: 1 addition & 0 deletions app/packages/core/src/plugins/SchemaIO/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ export { default as TupleView } from "./TupleView";
export { default as UnsupportedView } from "./UnsupportedView";
export { default as LazyFieldView } from "./LazyFieldView";
export { default as GridView } from "./GridView";
export { default as IconButtonView } from "./IconButtonView";
39 changes: 31 additions & 8 deletions app/packages/operators/src/OperatorIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { usePluginDefinition } from "@fiftyone/plugins";
import { useColorScheme } from "@mui/material";
import { IconProps, useColorScheme } from "@mui/material";
import { resolveServerPath } from "./utils";
import { MuiIconFont } from "@fiftyone/components";

export default function OperatorIcon(props: CustomIconPropsType) {
const {
Expand All @@ -11,26 +12,45 @@ export default function OperatorIcon(props: CustomIconPropsType) {
_builtIn,
Fallback,
canExecute,
iconProps,
} = props;
const { mode } = useColorScheme();
const iconPath = mode === "dark" && darkIcon ? darkIcon : lightIcon || icon;

if (!iconPath || !canExecute) return Fallback ? <Fallback /> : null;
if (_builtIn) return <ImageIcon src={iconPath} />;
return <CustomOperatorIcon pluginName={pluginName} iconPath={iconPath} />;
if (isIconFont(iconPath)) {
return <MuiIconFont {...(iconProps as IconProps)} name={iconPath} />;
} else if (!iconPath || !canExecute) {
return Fallback ? <Fallback /> : null;
} else if (_builtIn) {
return <ImageIcon src={iconPath} />;
} else {
return (
<CustomOperatorIcon
pluginName={pluginName}
iconPath={iconPath}
iconProps={iconProps as JSX.IntrinsicElements["img"]}
/>
);
}
}

function CustomOperatorIcon(props: CustomOperatorIconPropsType) {
const { pluginName, iconPath } = props;
const { pluginName, iconPath, iconProps } = props;
const plugin = usePluginDefinition(pluginName);
const assetPath = resolveServerPath(plugin);
const resolvedIconPath = assetPath + iconPath;
return <ImageIcon src={resolvedIconPath} />;
return <ImageIcon src={resolvedIconPath} iconProps={iconProps} />;
}

function ImageIcon(props: ImageIconPropsType) {
const { src } = props;
return <img src={src} height={21} width={21} />;
const { src, iconProps } = props;
return <img {...iconProps} src={src} height={21} width={21} />;
}

function isIconFont(path: string) {
return (
typeof path === "string" && !path?.includes("/") && !path?.includes(".")
);
}

export type CustomIconPropsType = {
Expand All @@ -41,13 +61,16 @@ export type CustomIconPropsType = {
_builtIn?: boolean;
Fallback?: React.ComponentType;
canExecute?: boolean;
iconProps?: IconProps | JSX.IntrinsicElements["img"];
};

type CustomOperatorIconPropsType = {
pluginName?: string;
iconPath?: string;
iconProps?: JSX.IntrinsicElements["img"];
};

type ImageIconPropsType = {
src: string;
iconProps?: JSX.IntrinsicElements["img"];
};
27 changes: 27 additions & 0 deletions app/packages/operators/src/Panel/register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PluginComponentType, registerComponent } from "@fiftyone/plugins";
import { defineCustomPanel } from "../CustomPanel";
import OperatorIcon from "../OperatorIcon";
import { ExecutionContext } from "../operators";

export default function registerPanel(ctx: ExecutionContext) {
registerComponent({
type: PluginComponentType.Panel,
name: ctx.params.panel_name,
component: defineCustomPanel(ctx.params),
label: ctx.params.panel_label,
activator: () => true,
Icon: () => {
return (
<OperatorIcon
icon={ctx.params.icon || "extension"}
darkIcon={ctx.params.dark_icon}
lightIcon={ctx.params.light_icon}
iconProps={{ sx: { fontSize: 14, mr: "0.5rem" } }}
/>
);
},
panelOptions: {
allowDuplicates: ctx.params.allow_duplicates,
},
});
}
38 changes: 11 additions & 27 deletions app/packages/operators/src/built-in-operators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
Layout,
SpaceNode,
usePanel,
usePanelState,
usePanels,
useSetPanelStateById,
Expand All @@ -13,9 +12,11 @@ import * as types from "./types";

import { toSlug } from "@fiftyone/utilities";
import copyToClipboard from "copy-to-clipboard";
import { merge } from "lodash";
import { useSetRecoilState } from "recoil";
import { useOperatorExecutor } from ".";
import useRefetchableSavedViews from "../../core/src/hooks/useRefetchableSavedViews";
import registerPanel from "./Panel/register";
import {
ExecutionContext,
Operator,
Expand All @@ -25,10 +26,7 @@ import {
executeOperator,
listLocalAndRemoteOperators,
} from "./operators";
import { usePromptOperatorInput, useShowOperatorIO } from "./state";
import { merge } from "lodash";
import { PluginComponentType, registerComponent } from "@fiftyone/plugins";
import CustomPanel, { defineCustomPanel } from "./CustomPanel";
import { useShowOperatorIO } from "./state";
import usePanelEvent from "./usePanelEvent";

//
Expand Down Expand Up @@ -957,41 +955,27 @@ class RegisterPanel extends Operator {
return new OperatorConfig({
name: "register_panel",
label: "Register panel",
// unlisted: true,
unlisted: true,
});
}
async resolveInput(ctx: ExecutionContext): Promise<types.Property> {
const inputs = new types.Object();
inputs.str("panel_name", { label: "Panel name", required: true });
inputs.str("panel_label", { label: "Panel label", required: true });
inputs.str("on_load", {
label: "On load operator",
// required: true,
});
inputs.str("on_change", {
label: "On change operator",
// required: true,
});
inputs.str("on_unload", {
label: "On unload operator",
});
inputs.str("icon", { label: "Icon" });
inputs.str("dark_icon", { label: "Icon for dark mode" });
inputs.str("light_icon", { label: "Icon for light mode" });
inputs.str("on_load", { label: "On load operator" });
inputs.str("on_change", { label: "On change operator" });
inputs.str("on_unload", { label: "On unload operator" });
inputs.bool("allow_duplicates", {
label: "Allow duplicates",
default: false,
});
return new types.Property(inputs);
}
async execute(ctx: ExecutionContext): Promise<void> {
registerComponent({
type: PluginComponentType.Panel,
name: ctx.params.panel_name,
component: defineCustomPanel(ctx.params),
label: ctx.params.panel_label,
activator: () => true,
panelOptions: {
allowDuplicates: ctx.params.allow_duplicates,
},
});
registerPanel(ctx);
Copy link
Contributor

@coderabbitai coderabbitai bot May 8, 2024

Choose a reason for hiding this comment

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

The use of registerPanel directly in the execute method of RegisterPanel class without any arguments seems incorrect. Typically, such functions require parameters to specify what is being registered.

- registerPanel(ctx);
+ registerPanel({
+   name: ctx.params.panel_name,
+   label: ctx.params.panel_label,
+   on_load: ctx.params.on_load,
+   on_change: ctx.params.on_change,
+   on_unload: ctx.params.on_unload,
+   allow_duplicates: ctx.params.allow_duplicates
+ });

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
registerPanel(ctx);
registerPanel({
name: ctx.params.panel_name,
label: ctx.params.panel_label,
on_load: ctx.params.on_load,
on_change: ctx.params.on_change,
on_unload: ctx.params.on_unload,
allow_duplicates: ctx.params.allow_duplicates
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The parameters are extracted and used in the registerPanel method itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

@imanjra, thank you for your patience. After reviewing the implementation of the registerPanel function, I can confirm that it indeed extracts and utilizes parameters from the ctx object internally. This means that your usage of registerPanel(ctx); in the execute method of the RegisterPanel class is correct as it passes the entire context which contains all necessary parameters.

I appreciate your clarification, and I'll remember this detail for future reviews.

Analysis chain
Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Search for the implementation of the `registerPanel` function in TypeScript files
rg "function registerPanel" --type ts

Length of output: 130



Script:

#!/bin/bash
# Fetch and display the content of register.tsx where registerPanel is defined
cat app/packages/operators/src/Panel/register.tsx

Length of output: 833


New Learnings Added
User: imanjra
PR: voxel51/fiftyone#4373
File: app/packages/operators/src/built-in-operators.ts:983-983
Timestamp: 2024-05-09T14:47:54.783Z
Learning: The `registerPanel` function in `app/packages/operators/src/Panel/register.tsx` extracts parameters from the `ctx` object internally and does not require explicit parameter passing when called.

Note: Learnings are effective only in the context of specific code segments. To apply general coding guidelines, please configure review instructions.

}
}

Expand Down
14 changes: 14 additions & 0 deletions app/packages/operators/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,20 @@ export class LazyFieldView extends View {
}
}

/**
* Operator class for describing a IconButtonView {@link Button} for an
* operator type.
*/
export class IconButtonView extends Button {
constructor(options: ViewProps) {
super(options);
this.name = "IconButtonView";
}
static fromJSON(json) {
return new IconButtonView(json);
}
}

/**
* Places where you can have your operator placement rendered.
*/
Expand Down
8 changes: 8 additions & 0 deletions app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2395,6 +2395,7 @@ __metadata:
"@types/react-syntax-highlighter": ^15.5.6
classnames: ^2.3.1
framer-motion: ^6.2.7
material-icons: ^1.13.12
path-to-regexp: ^6.2.0
prettier: ^2.7.1
react-input-autosize: ^3.0.0
Expand Down Expand Up @@ -13188,6 +13189,13 @@ __metadata:
languageName: node
linkType: hard

"material-icons@npm:^1.13.12":
version: 1.13.12
resolution: "material-icons@npm:1.13.12"
checksum: 46f0e03c4c22e8334c633915dd87c3023f0c6345c1d9376170aa8bb8c54e944a5fc40ec128888965b563e2472a8fae83a2f5b75d3f957ede8b5d8476650e4109
languageName: node
linkType: hard

"math-log2@npm:^1.0.1":
version: 1.0.1
resolution: "math-log2@npm:1.0.1"
Expand Down
10 changes: 10 additions & 0 deletions fiftyone/operators/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| `voxel51.com <https://voxel51.com/>`_
|
"""

import json

from bson import json_util
Expand Down Expand Up @@ -265,6 +266,9 @@ def register_panel(
self,
name,
label,
icon=None,
dark_icon=None,
light_icon=None,
on_load=None,
on_unload=None,
on_change=None,
Expand All @@ -275,6 +279,9 @@ def register_panel(

Args:
name: the name of the panel to register
icon (None): the icon to display in the panel tab
dark_icon (None): the icon to display in the panel tab in dark mode of app
light_icon (None): the icon to display in the panel tab in light mode of app
on_load (None): an operator to invoke when the panel is loaded
on_unload (None): an operator to invoke when the panel is unloaded
on_change (None): an operator to invoke when the panel state changes
Expand All @@ -283,6 +290,9 @@ def register_panel(
params = {
"panel_name": name,
"panel_label": label,
"icon": icon,
"dark_icon": dark_icon,
"light_icon": light_icon,
"on_load": on_load,
"on_unload": on_unload,
"on_change": on_change,
Expand Down