Skip to content
This repository has been archived by the owner on Mar 24, 2024. It is now read-only.

Commit

Permalink
Update PanelCatalogContext and providers to expose extension panels (#…
Browse files Browse the repository at this point in the history
…1082)

The panel list receives panels from the panel catalog. Previously the
panel catalog only exposed builtin panels. This change introduces
extension panels via the panel catalog.

Panel categories are removed in favor of one list of panels. Studio internal panels are hidden behind an application setting.
  • Loading branch information
defunctzombie committed May 27, 2021
1 parent ce37ee3 commit 1999c00
Show file tree
Hide file tree
Showing 18 changed files with 270 additions and 308 deletions.
9 changes: 3 additions & 6 deletions app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@ import ModalHost from "@foxglove/studio-base/context/ModalHost";
import { PlayerSourceDefinition } from "@foxglove/studio-base/context/PlayerSelectionContext";
import CurrentLayoutProvider from "@foxglove/studio-base/providers/CurrentLayoutProvider";
import ExtensionRegistryProvider from "@foxglove/studio-base/providers/ExtensionRegistryProvider";
import PanelCatalogProvider from "@foxglove/studio-base/providers/PanelCatalogProvider";
import URDFAssetLoader from "@foxglove/studio-base/services/URDFAssetLoader";
import getGlobalStore from "@foxglove/studio-base/store/getGlobalStore";

const BuiltinPanelCatalogProvider = React.lazy(
() => import("@foxglove/studio-base/context/BuiltinPanelCatalogProvider"),
);

type AppProps = {
availableSources: PlayerSourceDefinition[];
demoBagUrl?: string;
Expand Down Expand Up @@ -53,13 +50,13 @@ export default function App(props: AppProps): JSX.Element {
<NativeFileMenuPlayerSelection />
<DndProvider backend={HTML5Backend}>
<Suspense fallback={<></>}>
<BuiltinPanelCatalogProvider>
<PanelCatalogProvider>
<Workspace
demoBagUrl={props.demoBagUrl}
deepLinks={props.deepLinks}
onToolbarDoubleClick={props.onFullscreenToggle}
/>
</BuiltinPanelCatalogProvider>
</PanelCatalogProvider>
</Suspense>
</DndProvider>
</MultiProvider>
Expand Down
1 change: 1 addition & 0 deletions app/AppSetting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export enum AppSetting {
TELEMETRY_ENABLED = "telemetry.telemetryEnabled",
TIMEZONE = "timezone",
UNLIMITED_MEMORY_CACHE = "experimental.unlimited-memory-cache",
SHOW_DEBUG_PANELS = "showDebugPanels",
}
2 changes: 1 addition & 1 deletion app/PanelAPI/useConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function useConfig<Config>(): [Config, SaveConfig<Config>] {
const panelComponent = useMemo(
() =>
panelId != undefined
? panelCatalog.getComponentForType(getPanelTypeFromId(panelId))
? panelCatalog.getPanelByType(getPanelTypeFromId(panelId))?.component
: undefined,
[panelCatalog, panelId],
);
Expand Down
5 changes: 5 additions & 0 deletions app/components/ExperimentalFeatureSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const features: Feature[] = [
name: "Unlimited in-memory cache (requires restart)",
description: <>Fully buffer a bag into memory. This may use up a lot of system memory.</>,
},
{
key: AppSetting.SHOW_DEBUG_PANELS,
name: "Studio Debug Panels",
description: <>Show Studio debug panels in the add panel list.</>,
},
];

function ExperimentalFeatureItem(props: { feature: Feature }) {
Expand Down
9 changes: 6 additions & 3 deletions app/components/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,11 @@ export default function Panel<Config extends PanelConfig>(
const [fullScreenLocked, setFullScreenLocked] = useState(false);
const panelCatalog = usePanelCatalog();

const panelsByType = useMemo(() => panelCatalog.getPanelsByType(), [panelCatalog]);
const type = PanelComponent.panelType;
const title = useMemo(() => panelsByType.get(type)?.title ?? "", [panelsByType, type]);
const title = useMemo(
() => panelCatalog.getPanelByType(type)?.title ?? "",
[panelCatalog, type],
);

const [config, saveConfig] = useConfigById<Config>(childId, PanelComponent.defaultConfig);
const panelComponentConfig = useMemo(
Expand All @@ -166,7 +168,8 @@ export default function Panel<Config extends PanelConfig>(
// If such a panel already exists, we update it with the new props.
const openSiblingPanel = useCallback(
(panelType: string, siblingConfigCreator: (arg0: PanelConfig) => PanelConfig) => {
const siblingComponent = panelCatalog.getComponentForType(panelType);
const siblingPanel = panelCatalog.getPanelByType(panelType);
const siblingComponent = siblingPanel?.component;
if (!siblingComponent) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion app/components/PanelLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function UnconnectedPanelLayout(props: Props): React.ReactElement {
}
const type = getPanelTypeFromId(id);

const PanelComponent = panelCatalog.getComponentForType(type);
const PanelComponent = panelCatalog.getPanelByType(type)?.component;
const panel = PanelComponent ? (
<PanelComponent childId={id} tabId={tabId} />
) : (
Expand Down
26 changes: 9 additions & 17 deletions app/components/PanelList/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import PanelList from "@foxglove/studio-base/components/PanelList";
import CurrentLayoutContext from "@foxglove/studio-base/context/CurrentLayoutContext";
import PanelCatalogContext, {
PanelCatalog,
PanelCategory,
PanelInfo,
} from "@foxglove/studio-base/context/PanelCatalogContext";
import CurrentLayoutState, {
Expand All @@ -47,24 +46,17 @@ SamplePanel2.defaultConfig = {};
const MockPanel1 = Panel(SamplePanel1);
const MockPanel2 = Panel(SamplePanel2);

const allPanels = [
{ title: "Some Panel", component: MockPanel1 },
{ title: "Happy Panel", component: MockPanel2 },
];

class MockPanelCatalog implements PanelCatalog {
getPanelCategories(): PanelCategory[] {
return [
{ label: "VISUALIZATION", key: "visualization" },
{ label: "DEBUGGING", key: "debugging" },
];
}
getPanelsByCategory(): Map<string, PanelInfo[]> {
return new Map([
["visualization", [{ title: "Some Panel", component: MockPanel1 }]],
["debugging", [{ title: "Happy Panel", component: MockPanel2 }]],
]);
}
getPanelsByType(): Map<string, PanelInfo> {
return new Map();
getPanels(): PanelInfo[] {
return allPanels;
}
getComponentForType(_type: string): PanelInfo["component"] | undefined {
return undefined;
getPanelByType(type: string): PanelInfo | undefined {
return allPanels.find((panel) => panel.component.panelType === type);
}
}

Expand Down
122 changes: 58 additions & 64 deletions app/components/PanelList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
// You may not use this file except in compliance with the License.
import MagnifyIcon from "@mdi/svg/svg/magnify.svg";
import fuzzySort from "fuzzysort";
import { flatMap } from "lodash";
import { useCallback, useEffect, useMemo } from "react";
import { useEffect, useMemo } from "react";
import { useDrag } from "react-dnd";
import { MosaicDragType, MosaicPath } from "react-mosaic-component";
import styled from "styled-components";
Expand Down Expand Up @@ -184,28 +183,26 @@ type Props = {
};

// sanity checks to help panel authors debug issues
function verifyPanels(panelsByCategory: Map<string, PanelInfo[]>) {
function verifyPanels(panels: PanelInfo[]) {
const panelTypes: Map<
string,
{ component: React.ComponentType<any>; presetSettings?: PresetSettings }
> = new Map();
for (const panels of panelsByCategory.values()) {
for (const { component } of panels) {
const { name, displayName, panelType } = component;
const dispName = displayName ?? name ?? "<unnamed>";
if (panelType.length === 0) {
throw new Error(`Panel component ${dispName} must declare a unique \`static panelType\``);
}
const existingPanel = panelTypes.get(panelType);
if (existingPanel) {
const otherDisplayName =
existingPanel.component.displayName ?? existingPanel.component.name ?? "<unnamed>";
throw new Error(
`Two components have the same panelType ('${panelType}'): ${otherDisplayName} and ${dispName}`,
);
}
panelTypes.set(panelType, { component });
for (const { component } of panels) {
const { name, displayName, panelType } = component;
const dispName = displayName ?? name ?? "<unnamed>";
if (panelType.length === 0) {
throw new Error(`Panel component ${dispName} must declare a unique \`static panelType\``);
}
const existingPanel = panelTypes.get(panelType);
if (existingPanel) {
const otherDisplayName =
existingPanel.component.displayName ?? existingPanel.component.name ?? "<unnamed>";
throw new Error(
`Two components have the same panelType ('${panelType}'): ${otherDisplayName} and ${dispName}`,
);
}
panelTypes.set(panelType, { component });
}
}

Expand Down Expand Up @@ -240,58 +237,73 @@ function PanelList(props: Props): JSX.Element {
}, []);

const panelCatalog = usePanelCatalog();
const panelCategories = useMemo(() => panelCatalog.getPanelCategories(), [panelCatalog]);
const panelsByCategory = useMemo(() => panelCatalog.getPanelsByCategory(), [panelCatalog]);
const allPanels = useMemo(() => {
return panelCatalog.getPanels();
}, [panelCatalog]);
/*
const panelsByCategory = useMemo(() => {
const allPanels = panelCatalog.getPanels();
const panelsByCat = new Map<string, PanelInfo[]>();
for (const panel of allPanels) {
const existing = panelsByCat.get(panel.category ?? "misc") ?? [];
existing.push(panel);
panelsByCat.set(panel.category ?? "misc", existing);
}
return panelsByCat;
}, [panelCatalog]);
*/

//const panelCategories = useMemo(() => Array.from(panelsByCategory.keys()), [panelsByCategory]);

useEffect(() => {
verifyPanels(panelsByCategory);
}, [panelsByCategory]);

const getFilteredItemsForCategory = useCallback(
(key: string) => {
return searchQuery.length > 0
? fuzzySort
.go(searchQuery, panelsByCategory.get(key) ?? [], { key: "title" })
.map((searchResult) => searchResult.obj)
: panelsByCategory.get(key);
},
[panelsByCategory, searchQuery],
);
verifyPanels(allPanels);
}, [allPanels]);

/*
const filteredItemsByCategoryIdx = React.useMemo(
() => panelCategories.map(({ key }) => getFilteredItemsForCategory(key)),
() => panelCategories.map((category) => getFilteredItemsForCategory(category)),
[getFilteredItemsForCategory, panelCategories],
);
*/

/*
const noResults = React.useMemo(
() => filteredItemsByCategoryIdx.every((items) => !items || items.length === 0),
[filteredItemsByCategoryIdx],
);
*/

const filteredItems = React.useMemo(
() => flatMap(Object.values(filteredItemsByCategoryIdx)),
[filteredItemsByCategoryIdx],
);
const filteredPanels = React.useMemo(() => {
return searchQuery.length > 0
? fuzzySort
.go(searchQuery, allPanels, { key: "title" })
.map((searchResult) => searchResult.obj)
: allPanels;
}, [allPanels, searchQuery]);

const highlightedPanel = React.useMemo(
() => (highlightedPanelIdx != undefined ? filteredItems[highlightedPanelIdx] : undefined),
[filteredItems, highlightedPanelIdx],
() => (highlightedPanelIdx != undefined ? filteredPanels[highlightedPanelIdx] : undefined),
[filteredPanels, highlightedPanelIdx],
);

const noResults = filteredPanels.length === 0;

const onKeyDown = React.useCallback(
(e) => {
if (e.key === "ArrowDown" && highlightedPanelIdx != undefined) {
setHighlightedPanelIdx((highlightedPanelIdx + 1) % filteredItems.length);
setHighlightedPanelIdx((highlightedPanelIdx + 1) % filteredPanels.length);
} else if (e.key === "ArrowUp" && highlightedPanelIdx != undefined) {
const newIdx = (highlightedPanelIdx - 1) % (filteredItems.length - 1);
setHighlightedPanelIdx(newIdx >= 0 ? newIdx : filteredItems.length + newIdx);
const newIdx = (highlightedPanelIdx - 1) % (filteredPanels.length - 1);
setHighlightedPanelIdx(newIdx >= 0 ? newIdx : filteredPanels.length + newIdx);
} else if (e.key === "Enter" && highlightedPanel) {
const { component } = highlightedPanel;
onPanelSelect({
type: component.panelType,
});
}
},
[filteredItems.length, highlightedPanel, highlightedPanelIdx, onPanelSelect],
[filteredPanels.length, highlightedPanel, highlightedPanelIdx, onPanelSelect],
);

const displayPanelListItem = React.useCallback(
Expand Down Expand Up @@ -352,25 +364,7 @@ function PanelList(props: Props): JSX.Element {
</StickyDiv>
<SScrollContainer>
{noResults && <SEmptyState>No panels match search criteria.</SEmptyState>}
{panelCategories.map(({ label }, categoryIdx) => {
const prevItems = flatMap(filteredItemsByCategoryIdx.slice(0, categoryIdx));
const localFilteredItems = filteredItemsByCategoryIdx[categoryIdx];
if (!localFilteredItems || localFilteredItems.length === 0) {
return ReactNull;
}
return (
<div key={label} style={{ paddingTop: "8px" }}>
{categoryIdx !== 0 && prevItems.length > 0 && <hr />}
<Item
isHeader
style={categoryIdx === 0 || prevItems.length === 0 ? { paddingTop: 0 } : {}}
>
{label}
</Item>
{localFilteredItems.map(displayPanelListItem)}
</div>
);
})}
{filteredPanels.map(displayPanelListItem)}
</SScrollContainer>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion app/components/PanelSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function PanelSettings(): JSX.Element {
[selectedPanelId],
);
const panelInfo = useMemo(
() => (panelType != undefined ? panelCatalog.getPanelsByType().get(panelType) : undefined),
() => (panelType != undefined ? panelCatalog.getPanelByType(panelType) : undefined),
[panelCatalog, panelType],
);

Expand Down
19 changes: 0 additions & 19 deletions app/context/BuiltinPanelCatalogProvider.tsx

This file was deleted.

14 changes: 5 additions & 9 deletions app/context/PanelCatalogContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ export type PanelInfo = {
component: ComponentType<any> & PanelStatics<any>;
};

export type PanelCategory = {
label: string;
key: string;
};

// PanelCatalog describes the interface for getting available panels
export interface PanelCatalog {
getPanelCategories(): PanelCategory[];
getPanelsByCategory(): Map<string, PanelInfo[]>;
getPanelsByType(): Map<string, PanelInfo>;
getComponentForType(type: string): PanelInfo["component"] | undefined;
// get a list of the available panels
getPanels(): PanelInfo[];

// Get panel information for a specific panel type (i.e. 3d, map, image, etc)
getPanelByType(type: string): PanelInfo | undefined;
}

const PanelCatalogContext = createContext<PanelCatalog | undefined>(undefined);
Expand Down

0 comments on commit 1999c00

Please sign in to comment.