diff --git a/app/react/components/Widget/WidgetTabs.tsx b/app/react/components/Widget/WidgetTabs.tsx
new file mode 100644
index 000000000..094a41852
--- /dev/null
+++ b/app/react/components/Widget/WidgetTabs.tsx
@@ -0,0 +1,66 @@
+import { RawParams } from '@uirouter/react';
+import clsx from 'clsx';
+import { ReactNode } from 'react';
+
+import { Icon } from '@@/Icon';
+import { Link } from '@@/Link';
+
+export interface Tab {
+ name: string;
+ icon: ReactNode;
+ widget: ReactNode;
+ selectedTabParam: string;
+}
+
+interface Props {
+ currentTabIndex: number;
+ tabs: Tab[];
+}
+
+export function WidgetTabs({ currentTabIndex, tabs }: Props) {
+ // ensure that the selectedTab param is always valid
+ const invalidQueryParamValue = tabs.every(
+ (tab) => encodeURIComponent(tab.selectedTabParam) !== tab.selectedTabParam
+ );
+
+ if (invalidQueryParamValue) {
+ throw new Error('Invalid query param value for tab');
+ }
+
+ return (
+
+
+
+ {tabs.map(({ name, icon }, index) => (
+
+
+ {name}
+
+ ))}
+
+
+
+ );
+}
+
+// findSelectedTabIndex returns the index of the selected-tab, or 0 if none is selected
+export function findSelectedTabIndex(
+ { params }: { params: RawParams },
+ tabs: Tab[]
+) {
+ const selectedTabParam = params['selected-tab'] || tabs[0].selectedTabParam;
+ const currentTabIndex = tabs.findIndex(
+ (tab) => tab.selectedTabParam === selectedTabParam
+ );
+ return currentTabIndex || 0;
+}
diff --git a/app/react/components/Widget/index.ts b/app/react/components/Widget/index.ts
index 92164b168..accc92bdd 100644
--- a/app/react/components/Widget/index.ts
+++ b/app/react/components/Widget/index.ts
@@ -4,11 +4,13 @@ import { WidgetFooter } from './WidgetFooter';
import { WidgetTitle } from './WidgetTitle';
import { WidgetTaskbar } from './WidgetTaskbar';
import { Loading } from './Loading';
+import { WidgetTabs } from './WidgetTabs';
interface WithSubcomponents {
Body: typeof WidgetBody;
Footer: typeof WidgetFooter;
Title: typeof WidgetTitle;
+ Tabs: typeof WidgetTabs;
Taskbar: typeof WidgetTaskbar;
Loading: typeof Loading;
}
@@ -18,6 +20,7 @@ const Widget = MainComponent as typeof MainComponent & WithSubcomponents;
Widget.Body = WidgetBody;
Widget.Footer = WidgetFooter;
Widget.Title = WidgetTitle;
+Widget.Tabs = WidgetTabs;
Widget.Taskbar = WidgetTaskbar;
Widget.Loading = Loading;
@@ -26,6 +29,7 @@ export {
WidgetBody,
WidgetFooter,
WidgetTitle,
+ WidgetTabs,
WidgetTaskbar,
Loading,
};