diff --git a/src/mui-material.d.ts b/src/mui-material.d.ts index 49c7917f..e1abd22c 100644 --- a/src/mui-material.d.ts +++ b/src/mui-material.d.ts @@ -21,7 +21,19 @@ interface MeterPaletteOptions extends ExtendPaletteOptions<{ declare module "@mui/material/styles" { interface Palette { + [key: string]: Palette["primary"] & { + onColor?: string; + offColor?: string; + borderColor?: string; + lineColor?: string; + needleColor?: string; + selectedColor?: string; + deselectedColor?: string; + fillColor?: string; + emptyColor?: string; + }; arc: Palette["primary"]; + actionbutton: Palette["primary"]; boolbutton: Palette["primary"] & { onColor: string; offColor: string; @@ -64,6 +76,7 @@ declare module "@mui/material/styles" { } interface PaletteOptions { + actionbutton?: PaletteOptions["primary"]; arc?: PaletteOptions["primary"]; boolbutton?: Partial & { onColor: string; diff --git a/src/redux/csWebLibConfig.ts b/src/redux/csWebLibConfig.ts index 5ea135d3..c7eef05b 100644 --- a/src/redux/csWebLibConfig.ts +++ b/src/redux/csWebLibConfig.ts @@ -9,4 +9,5 @@ export type CsWebLibConfig = { THROTTLE_PERIOD: number | undefined; defaultMjpgEndpoint: string | undefined; csWebLibFeatureFlags: FeatureFlags; + classFile?: string; }; diff --git a/src/redux/slices/configurationSlice.ts b/src/redux/slices/configurationSlice.ts index 0744d86b..3c51bf10 100644 --- a/src/redux/slices/configurationSlice.ts +++ b/src/redux/slices/configurationSlice.ts @@ -5,7 +5,7 @@ import { CsWebLibConfig } from "../csWebLibConfig"; * This reducer is intended to be initialized via `preloadedState`. * The initialState here exists only as a type-safe fallback. */ -const initialState: CsWebLibConfig = { +export const initialState: CsWebLibConfig = { storeMode: "DEV", PVWS_SOCKET: "", PVWS_SSL: true, @@ -13,7 +13,8 @@ const initialState: CsWebLibConfig = { defaultMjpgEndpoint: "", csWebLibFeatureFlags: { enableDynamicScripts: false - } + }, + classFile: undefined }; /** @@ -31,7 +32,8 @@ export const configurationSlice = createSlice({ selectFeatureFlags: state => state.csWebLibFeatureFlags, selectEnableDynamicScripts: state => state.csWebLibFeatureFlags?.enableDynamicScripts ?? false, - selectDefaultMjpgEndpoint: state => state.defaultMjpgEndpoint + selectDefaultMjpgEndpoint: state => state.defaultMjpgEndpoint, + selectClassFile: state => state.classFile } }); @@ -41,5 +43,6 @@ export const { selectConfiguration, selectFeatureFlags, selectEnableDynamicScripts, - selectDefaultMjpgEndpoint + selectDefaultMjpgEndpoint, + selectClassFile } = configurationSlice.selectors; diff --git a/src/testResources.tsx b/src/testResources.tsx index 7f4e365f..93683694 100644 --- a/src/testResources.tsx +++ b/src/testResources.tsx @@ -20,6 +20,8 @@ import { NotificationStack } from "./redux/slices/notificationsSlice"; import { FileCache, FileCacheState } from "./redux/slices/fileCacheSlice"; +import { CsWebLibConfig } from "./redux"; +import { initialState } from "./redux/slices/configurationSlice"; // Helper functions for dtypes. export function ddouble( @@ -79,11 +81,13 @@ export const ACTIONS_EX_FIRST = { export const createRootStoreState = ( csState?: CsState, notifications?: NotificationStack, - fileCache?: FileCacheState + fileCache?: FileCacheState, + configuration?: CsWebLibConfig ) => ({ cs: csState ?? initialCsState, notifications: notifications ?? initialNotificationsState, - fileCache: fileCache ?? ({} as FileCache) + fileCache: fileCache ?? ({} as FileCache), + configuration: configuration ?? initialState }); export const contextWrapperGenerator = ( diff --git a/src/ui/hooks/useClassFile.test.tsx b/src/ui/hooks/useClassFile.test.tsx new file mode 100644 index 00000000..05d84e98 --- /dev/null +++ b/src/ui/hooks/useClassFile.test.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { contextRender, createRootStoreState } from "../../testResources"; +import { vi } from "vitest"; +import { act, screen } from "@testing-library/react"; +import { ensureWidgetsRegistered } from "../widgets"; +import { useClassFile } from "./useClassFile"; +import { CsWebLibConfig } from "../../redux"; +import { phoebusTheme } from "../../phoebusTheme"; +import { createTheme } from "@mui/material"; +import { getFileState } from "./useFile.test"; +ensureWidgetsRegistered(); + +const initialState: CsWebLibConfig = { + storeMode: "DEV", + PVWS_SOCKET: "", + PVWS_SSL: true, + THROTTLE_PERIOD: 100, + defaultMjpgEndpoint: "", + csWebLibFeatureFlags: { + enableDynamicScripts: false + } +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace NodeJS { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Global {} + } +} + +interface GlobalFetch extends NodeJS.Global { + fetch: any; +} +const globalWithFetch = global as GlobalFetch; + +const ClassFileTester = (): JSX.Element => { + const contents = useClassFile(); + return
contents: {JSON.stringify(contents.palette)}
; +}; + +describe("useClassFile", (): void => { + it("returns default phoebus theme if no classfile", (): void => { + const { getByText } = contextRender( + , + {}, + {}, + createRootStoreState(getFileState(), undefined, undefined, initialState), + {} + ); + + expect( + getByText(`contents: ${JSON.stringify(phoebusTheme.palette)}`) + ).toBeInTheDocument(); + }); + + it("returns theme with classes if classfile", async (): Promise => { + const mockSuccessResponse = ` + + Widget Classes + 0 + + MY_CLASS + 390 + 180 + + + + + + + + + $(actions) + + `; + const mockJsonPromise = Promise.resolve(mockSuccessResponse); + const mockFetchPromise = Promise.resolve({ + text: (): Promise => mockJsonPromise + }); + const mockFetch = (): Promise => mockFetchPromise; + vi.spyOn(globalWithFetch, "fetch").mockImplementation(mockFetch); + await act(async () => { + contextRender( + , + {}, + {}, + createRootStoreState(getFileState(), undefined, undefined, { + classFile: "myclass.bcf", + ...initialState + }), + {} + ); + }); + + const responseContent = JSON.stringify( + createTheme({ + customName: "class", + palette: { + ...phoebusTheme.palette, + ...{ + MY_CLASSactionbutton: { + main: "rgba(0,0,255,1)", + contrastText: "rgba(0,0,0,1)" + } + } + } + }).palette + ); + expect( + screen.getByText(`contents: ${responseContent}`) + ).toBeInTheDocument(); + }); +}); diff --git a/src/ui/hooks/useClassFile.tsx b/src/ui/hooks/useClassFile.tsx new file mode 100644 index 00000000..97c5e183 --- /dev/null +++ b/src/ui/hooks/useClassFile.tsx @@ -0,0 +1,89 @@ +import { useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import { createTheme, Theme } from "@mui/material"; +import { fetchAndConvert } from "./useFile"; +import { WidgetDescription } from "../widgets/createComponent"; +import { selectClassFile } from "../../redux/slices/configurationSlice"; +import { phoebusTheme } from "../../phoebusTheme"; + +// Map widget props to MUI theme props +const keyMap: Record = { + backgroundColor: "main", + foregroundColor: "contrastText" +}; + +const CLASS_PROPS = new Set([ + "offColor", + "onColor", + "foregroundColor", + "backgroundColor", + "lineColor", + "emptyColor", + "knobColor", + "color", + "fillColor", + "needleColor", + "selectedColor", + "deselectedColor", + "borderColor" +]); + +export function useClassFile(userTheme?: Theme): Theme { + const classFile = useSelector(selectClassFile); + const [theme, setTheme] = useState(userTheme ?? phoebusTheme); + + useEffect(() => { + const fetchData = async (): Promise => { + const widgetDescription = await fetchAndConvert( + classFile as string, + "ca", + {} + ); + setTheme(createClassPalettes(widgetDescription)); + }; + + if (classFile !== undefined) { + fetchData(); + } + }, [classFile, userTheme]); + + return theme; +} + +export function createClassPalettes(classFile: WidgetDescription): Theme { + // If classfile is empty, do nothing + if (!classFile.children) return phoebusTheme; + + const palette: { [key: string]: any } = {}; + classFile.children?.forEach((child: WidgetDescription) => { + const widgetType: string = child.type; + // Construct palette name from widget type and classname + const paletteName = `${child.name}${widgetType}`; + + // Only colors go in the theme palette + const matches = Object.entries(child) + .filter(([key]) => CLASS_PROPS.has(key)) + .map(([key, value]) => ({ key, value })); + + // Assign colors to palette + palette[paletteName] = { + // Put Phoebus theme defaults, overwrite with class props + ...phoebusTheme.palette[widgetType], + ...Object.fromEntries( + matches.map(({ key, value }) => [ + keyMap[key] ?? key, + value?.colorString ?? undefined + ]) + ) + }; + }); + // Create Theme + const classTheme = createTheme({ + customName: "class", + palette: { + ...phoebusTheme.palette, + ...palette + } + }); + return classTheme; +} diff --git a/src/ui/hooks/useFile.test.tsx b/src/ui/hooks/useFile.test.tsx index d5a214cd..146e24da 100644 --- a/src/ui/hooks/useFile.test.tsx +++ b/src/ui/hooks/useFile.test.tsx @@ -27,7 +27,7 @@ const FileTester = (props: { file: File }): JSX.Element => { return
contents: {JSON.stringify(contents)}
; }; -function getFileState(): CsState { +export function getFileState(): CsState { return { valueCache: {}, subscriptions: {}, diff --git a/src/ui/hooks/useFile.tsx b/src/ui/hooks/useFile.tsx index 9916b04b..fb9ed751 100644 --- a/src/ui/hooks/useFile.tsx +++ b/src/ui/hooks/useFile.tsx @@ -15,6 +15,7 @@ import { parseJson } from "../widgets/EmbeddedDisplay/jsonParser"; import { parseOpi } from "../widgets/EmbeddedDisplay/opiParser"; import { Store } from "redux"; import { newAbsolutePosition } from "../../types/position"; +import { parseBcf } from "../widgets/EmbeddedDisplay/bcfParser"; const EMPTY_WIDGET: WidgetDescription = { type: "shape", @@ -29,7 +30,7 @@ export interface File { defaultProtocol: string; } -async function fetchAndConvert( +export async function fetchAndConvert( filepath: string, protocol: string, macros?: MacroMap @@ -58,6 +59,9 @@ async function fetchAndConvert( filepath ); break; + case "bcf": + description = await parseBcf(contents, protocol, parentDir, filepath); + break; case "json": description = await parseJson( contents, diff --git a/src/ui/hooks/useStyle.test.ts b/src/ui/hooks/useStyle.test.ts index 844ed727..90b11579 100644 --- a/src/ui/hooks/useStyle.test.ts +++ b/src/ui/hooks/useStyle.test.ts @@ -20,6 +20,10 @@ const mockTheme = { main: "#654321", contrastText: "#000000", light: "#fedcba" + }, + MY_CLASSwidgetA: { + main: "#a52590", + contrastText: "#dd1c1c" } }, borders: { @@ -118,6 +122,13 @@ describe("useStyle", () => { }); }); + it("selects the class theme over default widget theme if class exists", () => { + const { result } = renderHook(() => useStyle({}, "widgetA", "MY_CLASS")); + + expect(result.current.colors.backgroundColor).toEqual("#a52590"); + expect(result.current.colors.color).toEqual("#dd1c1c"); + }); + it("uses provided font when fontToCss returns a value", () => { const font = newFont(12, FontStyle.Bold, "Liberation sans", "abc"); const { result } = renderHook(() => useStyle({ font })); diff --git a/src/ui/hooks/useStyle.ts b/src/ui/hooks/useStyle.ts index f189cfea..d9ceee59 100644 --- a/src/ui/hooks/useStyle.ts +++ b/src/ui/hooks/useStyle.ts @@ -21,10 +21,21 @@ export interface UseStyleResult { other: CSSProperties; } -const selectPalette = (theme: Theme, widgetName?: string): PaletteColor => { - if (theme?.palette && widgetName && widgetName in theme?.palette) { +interface UseStyleProps { + border?: Border; + font?: Font; + visible?: boolean; + foregroundColor?: Color; + backgroundColor?: Color; + transparent?: boolean; + actions?: WidgetActions; + customColors?: { [key: string]: Color | undefined }; +} + +const selectPalette = (theme: Theme, themeName?: string): PaletteColor => { + if (theme?.palette && themeName && themeName in theme?.palette) { return theme.palette[ - widgetName as keyof typeof theme.palette + themeName as keyof typeof theme.palette ] as PaletteColor; } @@ -61,20 +72,13 @@ const fontSelector = (theme: Theme, font?: Font): CSSProperties => * @returns a CSSProperties object to pass into another element under the style key */ export const useStyle = ( - props: { - border?: Border; - font?: Font; - visible?: boolean; - foregroundColor?: Color; - backgroundColor?: Color; - transparent?: boolean; - actions?: WidgetActions; - customColors?: { [key: string]: Color | undefined }; - }, - widgetName?: string + props: UseStyleProps, + widgetName?: string, + className?: string ): UseStyleResult => { const theme = useTheme(); - const themePalette = selectPalette(theme, widgetName); + const themeName = `${className ?? ""}${widgetName}`; + const themePalette = selectPalette(theme, themeName); const themeBorder = selectBorder(theme, widgetName); const propsBorder = borderToCss(props.border); const border = { @@ -86,15 +90,18 @@ export const useStyle = ( const visible = props.visible === undefined || props.visible; - const foregroundColor = foregroundColorSelector( - themePalette, - props?.foregroundColor - ); - const backgroundColor = backgroundColorSelector( - themePalette, - props?.backgroundColor, - props.transparent - ); + const classExists = className && themeName in theme?.palette; + // If palette for class exists, use that + const foregroundColor = classExists + ? themePalette.contrastText + : foregroundColorSelector(themePalette, props?.foregroundColor); + const backgroundColor = classExists + ? themePalette.main + : backgroundColorSelector( + themePalette, + props?.backgroundColor, + props.transparent + ); const customColors: { [key: string]: string } = Object.fromEntries( Object.entries(themePalette) diff --git a/src/ui/widgets/ActionButton/actionButton.tsx b/src/ui/widgets/ActionButton/actionButton.tsx index 6f73a3f6..c55cdf30 100644 --- a/src/ui/widgets/ActionButton/actionButton.tsx +++ b/src/ui/widgets/ActionButton/actionButton.tsx @@ -84,7 +84,8 @@ export const ActionButtonComponent = ( ...props, actions: props?.actions as WidgetActions | undefined }, - widgetName + widgetName, + props.class ); const { diff --git a/src/ui/widgets/Arc/arc.tsx b/src/ui/widgets/Arc/arc.tsx index 29547c3f..593c253e 100644 --- a/src/ui/widgets/Arc/arc.tsx +++ b/src/ui/widgets/Arc/arc.tsx @@ -30,7 +30,7 @@ const ArcProps = { }; export const ArcComponent = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { // CSS uses "Fill", Phoebus uses "transparent" const transparent = @@ -43,7 +43,8 @@ export const ArcComponent = ( foregroundColor: props?.lineColor ?? props?.foregroundColor, transparent }, - widgetName + widgetName, + props.class ); const { startAngle = 0, totalAngle = 90, lineWidth = 3 } = props; diff --git a/src/ui/widgets/BoolButton/boolButton.tsx b/src/ui/widgets/BoolButton/boolButton.tsx index cf99e883..06dbe11a 100644 --- a/src/ui/widgets/BoolButton/boolButton.tsx +++ b/src/ui/widgets/BoolButton/boolButton.tsx @@ -89,7 +89,8 @@ export const BoolButtonComponent = ( ...props, customColors: { onColor: props?.onColor, offColor: props?.offColor } }, - widgetName + widgetName, + props.class ); const { diff --git a/src/ui/widgets/ByteMonitor/byteMonitor.tsx b/src/ui/widgets/ByteMonitor/byteMonitor.tsx index d1264f14..8a78eace 100644 --- a/src/ui/widgets/ByteMonitor/byteMonitor.tsx +++ b/src/ui/widgets/ByteMonitor/byteMonitor.tsx @@ -62,7 +62,8 @@ export const ByteMonitorComponent = ( borderColor: props?.ledBorderColor } }, - widgetName + widgetName, + props.class ); const { value } = getPvValueAndName(pvData); diff --git a/src/ui/widgets/Checkbox/checkbox.tsx b/src/ui/widgets/Checkbox/checkbox.tsx index 0406e61a..1175ad21 100644 --- a/src/ui/widgets/Checkbox/checkbox.tsx +++ b/src/ui/widgets/Checkbox/checkbox.tsx @@ -67,7 +67,7 @@ export type CheckboxComponentProps = InferWidgetProps & export const CheckboxComponent = ( props: CheckboxComponentProps ): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const { enabled = true, label = "Label", pvData } = props; const { value, diff --git a/src/ui/widgets/ChoiceButton/choiceButton.tsx b/src/ui/widgets/ChoiceButton/choiceButton.tsx index e8bfa4f0..bd6b0da3 100644 --- a/src/ui/widgets/ChoiceButton/choiceButton.tsx +++ b/src/ui/widgets/ChoiceButton/choiceButton.tsx @@ -76,7 +76,8 @@ export const ChoiceButtonComponent = ( ): JSX.Element => { const style = useStyle( { ...props, customColors: { selectedColor: props?.selectedColor } }, - widgetName + widgetName, + props.class ); const { pvData, diff --git a/src/ui/widgets/Display/display.tsx b/src/ui/widgets/Display/display.tsx index 0a04e59b..c0cc1bb4 100644 --- a/src/ui/widgets/Display/display.tsx +++ b/src/ui/widgets/Display/display.tsx @@ -37,7 +37,7 @@ const DisplayProps = { // Generic display widget to put other things inside export const DisplayComponent = ( - props: InferWidgetProps & { id: string } + props: InferWidgetProps & { id: string; class?: string } ): JSX.Element => { // Macros specific to this display. Children of this component // can set macros by using the updateMacro function on the @@ -57,7 +57,7 @@ export const DisplayComponent = ( } }; - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); let extendedStyle: React.CSSProperties = { ...style.colors, diff --git a/src/ui/widgets/DynamicImage/demoImage.tsx b/src/ui/widgets/DynamicImage/demoImage.tsx index ac3aeb94..331e4fd8 100644 --- a/src/ui/widgets/DynamicImage/demoImage.tsx +++ b/src/ui/widgets/DynamicImage/demoImage.tsx @@ -61,7 +61,7 @@ export const DemoImageComponent = ( // dataHeight = 100, // visible = true // } = props; - const { colors } = useStyle(props, widgetName); + const { colors } = useStyle(props, widgetName, props.class); const { effectivePvName } = getPvValueAndName(props?.pvData); const urls = buildMjpgPvUrls(props?.mjpgEndpoints, effectivePvName); diff --git a/src/ui/widgets/EmbeddedDisplay/bcfParser.test.ts b/src/ui/widgets/EmbeddedDisplay/bcfParser.test.ts new file mode 100644 index 00000000..f05ab867 --- /dev/null +++ b/src/ui/widgets/EmbeddedDisplay/bcfParser.test.ts @@ -0,0 +1,85 @@ +import { ensureWidgetsRegistered } from ".."; +import { WidgetDescription } from "../createComponent"; +import { parseBcf } from "./bcfParser"; +ensureWidgetsRegistered(); + +const PREFIX = "prefix"; + +describe("bcf file parser", (): void => { + const bcfFileString = ` + + + Widget Classes + 0 + + DLS_PRIMARY + 30 + 133 + 184 + 40 + Primary button + + + + + + + + + + + + + + + + + + + + + + + MY_CLASS + 390 + 180 + + + + + + + + + $(actions) + + `; + + it("parses only class properties", async (): Promise => { + const classes = (await parseBcf( + bcfFileString, + "ca", + PREFIX + )) as WidgetDescription; + expect(classes.children?.length).toEqual(2); + + const label = classes.children?.[0] as WidgetDescription; + expect(label.name).toEqual("DLS_PRIMARY"); + // Colours survive as use class + expect(label.backgroundColor).toEqual({ + colorString: "rgba(29,41,69,1)" + }); + // Non class property not passed on + expect(label.offLabel).toEqual(undefined); + + const actionButton = classes.children?.[1] as WidgetDescription; + expect(actionButton.name).toEqual("MY_CLASS"); + // Colours survive as use class + expect(actionButton.backgroundColor).toEqual({ + colorString: "rgba(0,0,255,1)" + }); + + // Non class property not passed on + expect(actionButton.width).toEqual(undefined); + }); +}); diff --git a/src/ui/widgets/EmbeddedDisplay/bcfParser.ts b/src/ui/widgets/EmbeddedDisplay/bcfParser.ts new file mode 100644 index 00000000..85b9a0fb --- /dev/null +++ b/src/ui/widgets/EmbeddedDisplay/bcfParser.ts @@ -0,0 +1,94 @@ +import { xml2js, ElementCompact } from "xml-js"; +import { PV, newRelativePosition } from "../../../types"; +import { WidgetDescription } from "../createComponent"; +import { + BOB_COMPLEX_PARSERS, + BOB_SIMPLE_PARSERS, + bobGetTargetWidget, + bobParseTabs, + bobParseTraces, + bobParseYAxes +} from "./bobParser"; +import { XmlDescription, opiParsePvName, OPI_PATCHERS } from "./opiParser"; +import { ParserDict, parseChildProps, parseWidget } from "./parser"; + +export async function parseBcf( + xmlString: string, + defaultProtocol: string, + filepath: string, + fileId?: string +): Promise { + // Convert it to a "compact format" + const compactJSON = xml2js(xmlString, { + compact: true + }) as XmlDescription; + const display = + compactJSON?.display ?? + compactJSON?.displayResponsive ?? + compactJSON?.displayGridLayout; + const displayAttributes = { + ...display, + _attributes: { + ...display._attributes, + type: compactJSON?.display + ? "display" + : compactJSON?.displayResponsive + ? "displayResponsive" + : "displayGridLayout" + }, + x: { _text: "0" }, + y: { _text: "0" } + }; + + compactJSON["display"] = displayAttributes; + + const simpleParsers: ParserDict = { + ...BOB_SIMPLE_PARSERS, + pvMetadataList: [ + "pv_name", + (pvName: ElementCompact): { pvName: PV }[] => [ + { pvName: opiParsePvName(pvName, defaultProtocol) } + ] + ] + }; + + const complexParsers = { + ...BOB_COMPLEX_PARSERS, + traces: (props: ElementCompact) => bobParseTraces(props["traces"]), + axes: (props: ElementCompact) => bobParseYAxes(props["y_axes"]), + colors: (props: ElementCompact) => + parseChildProps(props["colors"], BOB_SIMPLE_PARSERS), + tabs: async (props: ElementCompact) => + bobParseTabs( + props["tabs"], + simpleParsers, + complexParsers, + filepath, + {}, + fileId + ) + }; + + const classFile = await parseWidget( + compactJSON.display, + bobGetTargetWidget, + "widget", + simpleParsers, + complexParsers, + false, + OPI_PATCHERS(BOB_SIMPLE_PARSERS, BOB_COMPLEX_PARSERS), + filepath, + {}, + fileId, + true + ); + + classFile.position = newRelativePosition( + classFile.position.x, + classFile.position.y, + classFile.position.width, + classFile.position.height + ); + + return classFile; +} diff --git a/src/ui/widgets/EmbeddedDisplay/bobParser.ts b/src/ui/widgets/EmbeddedDisplay/bobParser.ts index 26407f93..4b33d43a 100644 --- a/src/ui/widgets/EmbeddedDisplay/bobParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/bobParser.ts @@ -313,7 +313,7 @@ function bobParseSymbols(jsonProp: ElementCompact): string[] | string { * @param props list of props for this element * @returns a array of Trace objects */ -function bobParseTraces(props: any): Trace[] { +export function bobParseTraces(props: any): Trace[] { const traces: Trace[] = []; let parsedProps = {}; if (props) { @@ -358,7 +358,7 @@ function bobParseMarker(props: any): Markers { * @param props * @returns an array of Axis. */ -function bobParseYAxes(props: any): Axis[] { +export function bobParseYAxes(props: any): Axis[] { const axes: Axis[] = []; let parsedProps = {}; if (props) { @@ -440,7 +440,7 @@ export function bobParseRois(jsonProp: ElementCompact): Rois { * @param macros macro map associated with this file * @returns a tab object, with attached children */ -async function bobParseTabs( +export async function bobParseTabs( props: any, simpleParsers: ParserDict, complexParsers: ComplexParserDict, @@ -584,7 +584,7 @@ export function bobParseActions( return processedActions; } -function bobGetTargetWidget(props: any): { +export function bobGetTargetWidget(props: any): { widget: React.FC; widgetProps: any; } { @@ -684,7 +684,7 @@ export const BOB_SIMPLE_PARSERS: ParserDict = { activeTab: ["active_tab", bobParseNumber] }; -const BOB_COMPLEX_PARSERS: ComplexParserDict = { +export const BOB_COMPLEX_PARSERS: ComplexParserDict = { ...OPI_COMPLEX_PARSERS, type: bobParseType, position: bobParsePosition, diff --git a/src/ui/widgets/EmbeddedDisplay/embeddedDisplay.tsx b/src/ui/widgets/EmbeddedDisplay/embeddedDisplay.tsx index df364c89..8f8f2a54 100644 --- a/src/ui/widgets/EmbeddedDisplay/embeddedDisplay.tsx +++ b/src/ui/widgets/EmbeddedDisplay/embeddedDisplay.tsx @@ -26,10 +26,10 @@ import { GroupBoxComponent } from "../GroupBox/groupBox"; import { useId } from "react-id-generator"; import { getOptionalValue, trimFromString } from "../utils"; import { Theme, ThemeProvider } from "@mui/material"; -import { phoebusTheme } from "../../../phoebusTheme"; import { useFile, File } from "../../hooks/useFile"; import { recursiveResolve } from "../../hooks/useMacros"; import { useRules } from "../../hooks/useRules"; +import { useClassFile } from "../../hooks/useClassFile"; const RESIZE_STRINGS = [ "scroll-widget", @@ -85,6 +85,7 @@ export const EmbeddedDisplay = ( embeddedDisplayMacroContext.macros ); + const theme = useClassFile(props.theme); const resolvedProps = useRules(macroProps); const description = useFile( resolvedProps.file as File, @@ -278,7 +279,7 @@ export const EmbeddedDisplay = ( if (resolvedProps.border?.style === BorderStyle.GroupBox) { return ( - + + {component} diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index 3adeb4f2..e7f2046f 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -742,7 +742,8 @@ export const OPI_SIMPLE_PARSERS: ParserDict = { scaleFont: ["scale_font", opiParseFont], minimum: ["minimum", opiParseNumber], maximum: ["maximum", opiParseNumber], - groupName: ["group_name", opiParseString] + groupName: ["group_name", opiParseString], + class: ["class", opiParseString] }; /** diff --git a/src/ui/widgets/EmbeddedDisplay/parser.ts b/src/ui/widgets/EmbeddedDisplay/parser.ts index 524b6330..4920f5c3 100644 --- a/src/ui/widgets/EmbeddedDisplay/parser.ts +++ b/src/ui/widgets/EmbeddedDisplay/parser.ts @@ -74,7 +74,8 @@ export async function genericParser( complexParsers: ComplexParserDict, // Whether props with no registered function should be passed through // with no parsing. - passThrough: boolean + passThrough: boolean, + classFile?: boolean ): Promise { const newProps: any = { type: targetWidget.widget }; const allProps = { @@ -91,7 +92,12 @@ export async function genericParser( try { if (widget.hasOwnProperty(opiPropName)) { if (!isEmpty(widget[opiPropName])) { - newProps[prop] = await propParser(widget[opiPropName]); + const attributes = widget[opiPropName]._attributes; + // If BCF file, only parse class attributes + const shouldParse = + !classFile || attributes?.use_class || opiPropName === "name"; + if (shouldParse) + newProps[prop] = await propParser(widget[opiPropName]); log.debug(`result ${newProps[prop]}`); // For certain simple string props we want to accept an empty value e.g. text } else if ( @@ -184,7 +190,8 @@ export async function parseWidget( patchFunctions: PatchFunction[], filepath?: string, macros?: MacroMap, - fileId?: string + fileId?: string, + classFile?: boolean ): Promise { const targetWidget = getTargetWidget(props); const allowedProps = { position: PositionProp, ...targetWidget?.widgetProps }; @@ -193,7 +200,8 @@ export async function parseWidget( targetWidget, simpleParsers, complexParsers, - passThrough + passThrough, + classFile ); // Execute patch functions. for (const patcher of patchFunctions) { @@ -218,7 +226,8 @@ export async function parseWidget( patchFunctions, filepath, macros, - fileId + fileId, + classFile ); }) ); diff --git a/src/ui/widgets/GroupingContainer/groupingContainer.tsx b/src/ui/widgets/GroupingContainer/groupingContainer.tsx index 66d48654..32d95f13 100644 --- a/src/ui/widgets/GroupingContainer/groupingContainer.tsx +++ b/src/ui/widgets/GroupingContainer/groupingContainer.tsx @@ -22,9 +22,9 @@ const GroupingContainerProps = { // Generic display widget to put other things inside export const GroupingContainerComponent = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); // Include and override parent macros with those from the prop. const { updateMacro, macros } = useContext(MacroContext); diff --git a/src/ui/widgets/Image/image.tsx b/src/ui/widgets/Image/image.tsx index 987fa79e..b27a6fbb 100644 --- a/src/ui/widgets/Image/image.tsx +++ b/src/ui/widgets/Image/image.tsx @@ -38,7 +38,7 @@ const ImageProps = { }; export const ImageComponent = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { const { rotation = 0, @@ -57,7 +57,7 @@ export const ImageComponent = ( const overflow = props.overflow ? "visible" : "hidden"; - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const fullStyle: CSSProperties = { ...style.colors, ...style.font, diff --git a/src/ui/widgets/Input/input.tsx b/src/ui/widgets/Input/input.tsx index 902a5c25..d23e3bd5 100644 --- a/src/ui/widgets/Input/input.tsx +++ b/src/ui/widgets/Input/input.tsx @@ -87,7 +87,7 @@ const TextField = styled(MuiTextField)({ export const SmartInputComponent = ( props: PVComponent & InferWidgetProps ): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const { precision = -1, diff --git a/src/ui/widgets/LED/led.tsx b/src/ui/widgets/LED/led.tsx index a7e4fe15..0316a498 100644 --- a/src/ui/widgets/LED/led.tsx +++ b/src/ui/widgets/LED/led.tsx @@ -51,7 +51,8 @@ export const LedComponent = (props: LedComponentProps): JSX.Element => { lineColor: props?.lineColor } }, - widgetName + widgetName, + props.class ); const { value } = getPvValueAndName(pvData); diff --git a/src/ui/widgets/Label/label.tsx b/src/ui/widgets/Label/label.tsx index 60e771aa..a90fcef9 100644 --- a/src/ui/widgets/Label/label.tsx +++ b/src/ui/widgets/Label/label.tsx @@ -54,12 +54,13 @@ const Typography = styled(MuiTypography)({ }); export const LabelComponent = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { // Default labels to transparent. const style = useStyle( { ...props, transparent: props.transparent ?? true }, - widgetName + widgetName, + props.class ); const { diff --git a/src/ui/widgets/Line/line.tsx b/src/ui/widgets/Line/line.tsx index de18645c..602accab 100644 --- a/src/ui/widgets/Line/line.tsx +++ b/src/ui/widgets/Line/line.tsx @@ -49,7 +49,7 @@ export const LineComponent = (props: LineComponentProps): JSX.Element => { lineStyle = 0 } = props; - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const color = transparent ? "transparent" diff --git a/src/ui/widgets/LinearMeter/linearMeter.tsx b/src/ui/widgets/LinearMeter/linearMeter.tsx index 16c9bf44..c3903456 100644 --- a/src/ui/widgets/LinearMeter/linearMeter.tsx +++ b/src/ui/widgets/LinearMeter/linearMeter.tsx @@ -79,7 +79,8 @@ export const LinearMeterComponent = ( knobColor: props?.colors?.knobColor as Color | undefined } }, - widgetName + widgetName, + props.class ); const { diff --git a/src/ui/widgets/MenuButton/menuButton.tsx b/src/ui/widgets/MenuButton/menuButton.tsx index 0188b95e..78ae31ae 100644 --- a/src/ui/widgets/MenuButton/menuButton.tsx +++ b/src/ui/widgets/MenuButton/menuButton.tsx @@ -54,7 +54,8 @@ export const MenuButtonComponent = ( ): JSX.Element => { const style = useStyle( { ...props, actions: props.actions as WidgetActions }, - widgetName + widgetName, + props.class ); const files = useContext(FileContext); const { diff --git a/src/ui/widgets/Meter/meter.tsx b/src/ui/widgets/Meter/meter.tsx index 287e33a6..0b12ccd9 100644 --- a/src/ui/widgets/Meter/meter.tsx +++ b/src/ui/widgets/Meter/meter.tsx @@ -57,7 +57,8 @@ export const MeterComponent = ( const style = useStyle( { ...props, customColors: { needleColor: props?.needleColor } }, - widgetName + widgetName, + props.class ); const { value } = getPvValueAndName(pvData); diff --git a/src/ui/widgets/Polygon/polygon.tsx b/src/ui/widgets/Polygon/polygon.tsx index a835da00..ee549543 100644 --- a/src/ui/widgets/Polygon/polygon.tsx +++ b/src/ui/widgets/Polygon/polygon.tsx @@ -28,11 +28,12 @@ const PolygonProps = { }; export const PolygonComponent = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { const { colors, customColors } = useStyle( { ...props, customColors: { lineColor: props?.lineColor } }, - widgetName + widgetName, + props.class ); const { lineWidth = 3, points, rotationAngle = 0 } = props; //Loop over points and convert to string for svg diff --git a/src/ui/widgets/ProgressBar/progressBar.tsx b/src/ui/widgets/ProgressBar/progressBar.tsx index ec1c68c9..01399e07 100644 --- a/src/ui/widgets/ProgressBar/progressBar.tsx +++ b/src/ui/widgets/ProgressBar/progressBar.tsx @@ -38,7 +38,8 @@ export const ProgressBarComponent = ( ): JSX.Element => { const style = useStyle( { ...props, customColors: { fillColor: props?.fillColor } }, - widgetName + widgetName, + props.class ); const { pvData, diff --git a/src/ui/widgets/Readback/readback.tsx b/src/ui/widgets/Readback/readback.tsx index 5a8cf47e..bf805ef0 100644 --- a/src/ui/widgets/Readback/readback.tsx +++ b/src/ui/widgets/Readback/readback.tsx @@ -88,7 +88,7 @@ export type ReadbackComponentProps = InferWidgetProps & export const ReadbackComponent = ( props: ReadbackComponentProps ): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const { enabled = true, pvData, diff --git a/src/ui/widgets/Shape/shape.tsx b/src/ui/widgets/Shape/shape.tsx index 58d47979..cc7a6ba6 100644 --- a/src/ui/widgets/Shape/shape.tsx +++ b/src/ui/widgets/Shape/shape.tsx @@ -32,7 +32,7 @@ const ShapeProps = { }; export const ShapeComponent = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { const { visible = true } = props; @@ -47,7 +47,8 @@ export const ShapeComponent = ( }, visible: visible }, - widgetName + widgetName, + props.class ); // Style overrides - Calculate radii of corners diff --git a/src/ui/widgets/SlideButton/slideButton.tsx b/src/ui/widgets/SlideButton/slideButton.tsx index 6e7d4714..7b8b719f 100644 --- a/src/ui/widgets/SlideButton/slideButton.tsx +++ b/src/ui/widgets/SlideButton/slideButton.tsx @@ -45,7 +45,8 @@ export const SlideButtonComponent = ( ...props, customColors: { onColor: props?.onColor, offColor: props?.offColor } }, - widgetName + widgetName, + props.class ); const { diff --git a/src/ui/widgets/SlideControl/slideControl.tsx b/src/ui/widgets/SlideControl/slideControl.tsx index 56a5d3a4..000d4b49 100644 --- a/src/ui/widgets/SlideControl/slideControl.tsx +++ b/src/ui/widgets/SlideControl/slideControl.tsx @@ -53,7 +53,7 @@ export const SliderControlProps = { export const SlideControlComponent = ( props: InferWidgetProps & PVComponent ): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const { pvData, enabled = true, diff --git a/src/ui/widgets/StripChart/stripChart.tsx b/src/ui/widgets/StripChart/stripChart.tsx index 102a4209..f7bd7fb1 100644 --- a/src/ui/widgets/StripChart/stripChart.tsx +++ b/src/ui/widgets/StripChart/stripChart.tsx @@ -73,7 +73,7 @@ export interface TimeSeriesPoint { export const StripChartComponent = ( props: StripChartComponentProps ): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const { traces, diff --git a/src/ui/widgets/Symbol/symbol.tsx b/src/ui/widgets/Symbol/symbol.tsx index e056bec0..01921a53 100644 --- a/src/ui/widgets/Symbol/symbol.tsx +++ b/src/ui/widgets/Symbol/symbol.tsx @@ -95,7 +95,8 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { transparent: props?.transparent ?? true, actions: props?.actions as WidgetActions | undefined }, - widgetName + widgetName, + props.class ); const { value } = getPvValueAndName(pvData); diff --git a/src/ui/widgets/Tabs/tabContainer.tsx b/src/ui/widgets/Tabs/tabContainer.tsx index f0e67a39..904bcb09 100644 --- a/src/ui/widgets/Tabs/tabContainer.tsx +++ b/src/ui/widgets/Tabs/tabContainer.tsx @@ -46,9 +46,10 @@ export const TabContainerComponent = ( props: InferWidgetProps & { fileId: string; id: string; + class?: string; } ): JSX.Element => { - const { colors } = useStyle(props, widgetName); + const { colors } = useStyle(props, widgetName, props.class); const { tabHeight = 30, width = WIDGET_DEFAULT_SIZES["tabs"][0], diff --git a/src/ui/widgets/Tabs/tabs.tsx b/src/ui/widgets/Tabs/tabs.tsx index bc5c65a2..a0b226bd 100644 --- a/src/ui/widgets/Tabs/tabs.tsx +++ b/src/ui/widgets/Tabs/tabs.tsx @@ -53,7 +53,7 @@ export const TabBarProps = { }; export const TabBar = ( - props: InferWidgetProps + props: InferWidgetProps & { class?: string } ): JSX.Element => { const { font, customColors } = useStyle( { @@ -63,7 +63,8 @@ export const TabBar = ( deselectedColor: props?.deselectedColor } }, - widgetName + widgetName, + props.class ); const { direction = 0, diff --git a/src/ui/widgets/Tank/tank.tsx b/src/ui/widgets/Tank/tank.tsx index 882995df..f27b4dbe 100644 --- a/src/ui/widgets/Tank/tank.tsx +++ b/src/ui/widgets/Tank/tank.tsx @@ -49,7 +49,8 @@ export const TankComponent = ( emptyColor: props?.emptyColor } }, - widgetName + widgetName, + props.class ); const { diff --git a/src/ui/widgets/Thermometer/thermometer.tsx b/src/ui/widgets/Thermometer/thermometer.tsx index 8a09f966..022652f4 100644 --- a/src/ui/widgets/Thermometer/thermometer.tsx +++ b/src/ui/widgets/Thermometer/thermometer.tsx @@ -48,7 +48,11 @@ export const ThermometerComponent = ( props: InferWidgetProps & PVComponent ): JSX.Element => { const svgRef = useRef(null); - const style = useStyle({ foregroundColor: props.fillColor }, widgetName); + const style = useStyle( + { foregroundColor: props.fillColor }, + widgetName, + props.class + ); const { pvData, limitsFromPv = false } = props; const { value } = getPvValueAndName(pvData); diff --git a/src/ui/widgets/XYPlot/xyPlot.tsx b/src/ui/widgets/XYPlot/xyPlot.tsx index c544e2c8..f55afdbb 100644 --- a/src/ui/widgets/XYPlot/xyPlot.tsx +++ b/src/ui/widgets/XYPlot/xyPlot.tsx @@ -81,7 +81,7 @@ export interface TimeSeriesPoint { } export const XYPlotComponent = (props: XYPlotComponentProps): JSX.Element => { - const style = useStyle(props, widgetName); + const style = useStyle(props, widgetName, props.class); const { traces, diff --git a/src/ui/widgets/widgetProps.ts b/src/ui/widgets/widgetProps.ts index 01af093b..8463583e 100644 --- a/src/ui/widgets/widgetProps.ts +++ b/src/ui/widgets/widgetProps.ts @@ -26,7 +26,9 @@ const BasicPropsType = { tooltip: StringPropOpt, border: BorderPropOpt, visible: BoolPropOpt, - mjpgEndpoints: StringArrayPropOpt + mjpgEndpoints: StringArrayPropOpt, + class: StringPropOpt, + name: StringPropOpt }; const PositionPropsType = { @@ -67,6 +69,7 @@ type BaseWidgetProps = { type ComponentProps = { style?: Record; + class?: string; }; // Props used by the ConnectingComponentWidget wrapper