Initial implementation of two-way communication with Fabric host (#1622)

* Listen to iframe messages. Test posting message.

* Plug new container message to show New Container dialog

* Rename message action to type

* Fix format

* Fix format

* Remove console.log() statement

* Rework fabric init flow. Implement open Collection Tab from fabric.

* Rename method to better match its purpose

* Update src/hooks/useKnockoutExplorer.ts

Use connectionString from message

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>

* Fix format

* For openTab action open first collection if not specified. Clean up FabricContract.

* Reformat FabricContracts

* Highlight current node selection using them token

* Reformat

* Automatically expand nodes in resource tree if underlying database or collection is expanded. Fix AllowedOrigins. Cleanup code.

* Fix format

* Fix lint issue

* Don't show the home screen for Fabric (#1636)

* Fix formatting

* Database name to open can be overridden by value in session storage

---------

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>
This commit is contained in:
Laurent Nguyen 2023-09-28 15:26:50 +00:00 committed by GitHub
parent dfdb44bdc9
commit d9e142d7a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 817 additions and 651 deletions

1182
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.14.3", "@fluentui/react": "8.14.3",
"@fluentui/react-components": "9.30.1", "@fluentui/react-components": "9.32.1",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3", "@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1", "@microsoft/applicationinsights-web": "2.6.1",
@ -236,4 +236,4 @@
"printWidth": 120, "printWidth": 120,
"endOfLine": "auto" "endOfLine": "auto"
} }
} }

View File

@ -61,6 +61,9 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`, `^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/.*\\.analysis-df\\.net$`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",

View File

@ -0,0 +1,25 @@
export type FabricMessage =
| {
type: "newContainer";
databaseName: string;
}
| {
type: "initialize";
connectionString: string | undefined;
}
| {
type: "openTab";
databaseName: string;
collectionName: string | undefined;
};
export type DataExploreMessage =
| "ready"
| {
type: number;
data: {
action: "LoadDatabases";
actionModifier: "success" | "start";
defaultExperience: "SQL";
};
};

View File

@ -9,8 +9,11 @@ import {
Tree, Tree,
TreeItem, TreeItem,
TreeItemLayout, TreeItemLayout,
TreeOpenChangeData,
TreeOpenChangeEvent,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { MoreHorizontal20Regular } from "@fluentui/react-icons"; import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { tokens } from "@fluentui/react-theme";
import * as React from "react"; import * as React from "react";
export interface TreeNode2MenuItem { export interface TreeNode2MenuItem {
@ -27,7 +30,7 @@ export interface TreeNode2 {
children?: TreeNode2[]; children?: TreeNode2[];
contextMenu?: TreeNode2MenuItem[]; contextMenu?: TreeNode2MenuItem[];
iconSrc?: string; iconSrc?: string;
// isExpanded?: boolean; isExpanded?: boolean;
className?: string; className?: string;
isAlphaSorted?: boolean; isAlphaSorted?: boolean;
// data?: any; // Piece of data corresponding to this node // data?: any; // Piece of data corresponding to this node
@ -37,7 +40,7 @@ export interface TreeNode2 {
isScrollable?: boolean; isScrollable?: boolean;
isSelected?: () => boolean; isSelected?: () => boolean;
onClick?: () => void; // Only if a leaf, other click will expand/collapse onClick?: () => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void; onExpanded?: () => Promise<void>;
onCollapsed?: () => void; onCollapsed?: () => void;
onContextMenuOpen?: () => void; onContextMenuOpen?: () => void;
} }
@ -46,7 +49,6 @@ export interface TreeNode2ComponentProps {
node: TreeNode2; node: TreeNode2;
className?: string; className?: string;
treeNodeId: string; treeNodeId: string;
globalOpenIds: string[];
} }
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 20, height: 20 }} />; const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 20, height: 20 }} />;
@ -54,20 +56,8 @@ const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt=""
export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({ export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
node, node,
treeNodeId, treeNodeId,
globalOpenIds,
}: TreeNode2ComponentProps): JSX.Element => { }: TreeNode2ComponentProps): JSX.Element => {
// const defaultOpenItems = node.isExpanded ? children?.map((child: TreeNode2) => child.label) : undefined; const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [isExpanded, setIsExpanded] = React.useState<boolean>(false);
// Compute whether node is expanded
React.useEffect(() => {
const isNowExpanded = globalOpenIds && globalOpenIds.includes(treeNodeId);
if (!isExpanded && isNowExpanded) {
// Catch the transition non-expanded to expanded
node.onExpanded?.();
}
setIsExpanded(isNowExpanded);
}, [globalOpenIds, treeNodeId, node, isExpanded]);
const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => { const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => {
if (!treeNode || !treeNode.children) { if (!treeNode || !treeNode.children) {
@ -95,8 +85,24 @@ export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
return unsortedChildren; return unsortedChildren;
}; };
const onOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (!node.isExpanded && data.open && node.onExpanded) {
// Catch the transition non-expanded to expanded
setIsLoading(true);
node.onExpanded?.().then(() => setIsLoading(false));
} else if (node.isExpanded && !data.open && node.onCollapsed) {
// Catch the transition expanded to non-expanded
node.onCollapsed?.();
}
};
return ( return (
<TreeItem value={treeNodeId} itemType={node.children !== undefined ? "branch" : "leaf"} style={{ height: "100%" }}> <TreeItem
value={treeNodeId}
itemType={node.children !== undefined ? "branch" : "leaf"}
style={{ height: "100%" }}
onOpenChange={onOpenChange}
>
<TreeItemLayout <TreeItemLayout
className={node.className} className={node.className}
actions={ actions={
@ -117,22 +123,21 @@ export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
</Menu> </Menu>
) )
} }
expandIcon={node.isLoading ? <Spinner size="extra-tiny" /> : undefined} expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)} iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
style={{
backgroundColor: node.isSelected && node.isSelected() ? tokens.colorNeutralBackground1Selected : undefined,
}}
> >
<span onClick={() => node.onClick?.()}>{node.label}</span> <span onClick={() => node.onClick?.()}>{node.label}</span>
</TreeItemLayout> </TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && ( {!node.isLoading && node.children?.length > 0 && (
<Tree <Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
// defaultOpenItems={defaultOpenItems}
style={{ overflow: node.isScrollable ? "auto" : undefined }}
>
{getSortedChildren(node).map((childNode: TreeNode2) => ( {getSortedChildren(node).map((childNode: TreeNode2) => (
<TreeNode2Component <TreeNode2Component
key={childNode.label} key={childNode.label}
node={childNode} node={childNode}
treeNodeId={`${treeNodeId}/${childNode.label}`} treeNodeId={`${treeNodeId}/${childNode.label}`}
globalOpenIds={globalOpenIds}
/> />
))} ))}
</Tree> </Tree>

View File

@ -8,7 +8,7 @@ import {
TreeOpenChangeEvent, TreeOpenChangeEvent,
createLightTheme, createLightTheme,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component"; import { TreeNode2, TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component";
import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes"; import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes";
import * as React from "react"; import * as React from "react";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
@ -69,16 +69,45 @@ export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: Resour
); );
// const { activeTab } = useTabs(); // const { activeTab } = useTabs();
const databaseTreeNodes = useDatabaseTreeNodes(container, isNotebookEnabled); const databaseTreeNodes = useDatabaseTreeNodes(container, isNotebookEnabled);
const dataNodeTree = { const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([DATA_TREE_LABEL]);
const dataNodeTree: TreeNode2 = {
id: "data", id: "data",
label: DATA_TREE_LABEL, label: DATA_TREE_LABEL,
isExpanded: true,
className: "accordionItemHeader", className: "accordionItemHeader",
children: databaseTreeNodes, children: databaseTreeNodes,
isScrollable: true, isScrollable: true,
}; };
const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([DATA_TREE_LABEL]); React.useEffect(() => {
// Compute open items based on node.isExpanded
const updateOpenItems = (node: TreeNode2, parentNodeId: string): void => {
// This will look for ANY expanded node, event if its parent node isn't expanded
// and add it to the openItems list
const globalId = parentNodeId === undefined ? node.label : `${parentNodeId}/${node.label}`;
if (node.isExpanded) {
let found = false;
for (const id of openItems) {
if (id === globalId) {
found = true;
break;
}
}
if (!found) {
setOpenItems((prevOpenItems) => [...prevOpenItems, globalId]);
}
}
if (node.children) {
for (const child of node.children) {
updateOpenItems(child, globalId);
}
}
};
updateOpenItems(dataNodeTree, undefined);
}, [databaseTreeNodes]);
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => setOpenItems(data.openItems); const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => setOpenItems(data.openItems);
@ -93,13 +122,7 @@ export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: Resour
style={{ height: "100%", width: "290px" }} style={{ height: "100%", width: "290px" }}
> >
{[dataNodeTree].map((node) => ( {[dataNodeTree].map((node) => (
<TreeNode2Component <TreeNode2Component key={node.label} className="dataResourceTree" node={node} treeNodeId={node.label} />
key={node.label}
className="dataResourceTree"
node={node}
treeNodeId={node.label}
globalOpenIds={[...openItems].map((item) => item.toString())}
/>
))} ))}
</Tree> </Tree>
</FluentProvider> </FluentProvider>

View File

@ -45,7 +45,7 @@ export const buildCollectionNode = (
// push to most recent // push to most recent
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection); mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
}, },
onExpanded: () => { onExpanded: async () => {
// Rewritten version of expandCollapseCollection // Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection); useSelectedNode.getState().setSelectedNode(collection);
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
@ -53,9 +53,16 @@ export const buildCollectionNode = (
(tab: TabsBase) => (tab: TabsBase) =>
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
); );
useDatabases.getState().updateDatabase(database);
}, },
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()), isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
onCollapsed: () => {
collection.collapseCollection();
// useCommandBar.getState().setContextButtons([]);
useDatabases.getState().updateDatabase(database);
},
isExpanded: collection.isCollectionExpanded(),
}; };
}; };

View File

@ -25,15 +25,22 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
onExpanded: async () => { onExpanded: async () => {
useSelectedNode.getState().setSelectedNode(database); useSelectedNode.getState().setSelectedNode(database);
if (databaseNode.children?.length === 0) { if (!databaseNode.children || databaseNode.children?.length === 0) {
databaseNode.isLoading = true; databaseNode.isLoading = true;
} }
await database.expandDatabase(); await database.expandDatabase();
databaseNode.isLoading = false; databaseNode.isLoading = false;
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id()); refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
useDatabases.getState().updateDatabase(database);
}, },
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database), onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(database),
isExpanded: database.isDatabaseExpanded(),
onCollapsed: () => {
database.collapseDatabase();
// useCommandBar.getState().setContextButtons([]);
useDatabases.getState().updateDatabase(database);
},
}; };
if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) { if (database.isDatabaseShared() && configContext.platform !== Platform.Fabric) {

View File

@ -1,5 +1,7 @@
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { FabricMessage } from "Contracts/FabricContract";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useSelectedNode } from "Explorer/useSelectedNode";
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer"; import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { fetchAccessData } from "hooks/usePortalAccessToken"; import { fetchAccessData } from "hooks/usePortalAccessToken";
@ -10,7 +12,7 @@ import { AccountKind, Flights } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendMessage, sendReadyMessage } from "../Common/MessageHandler"; import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
import { Platform, configContext, updateConfigContext } from "../ConfigContext"; import { Platform, configContext, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction, TabKind } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
import { handleOpenAction } from "../Explorer/OpenActions/OpenActions"; import { handleOpenAction } from "../Explorer/OpenActions/OpenActions";
@ -63,24 +65,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
const explorer = await configurePortal(); const explorer = await configurePortal();
setExplorer(explorer); setExplorer(explorer);
} else if (platform === Platform.Fabric) { } else if (platform === Platform.Fabric) {
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer const explorer = await configureFabric();
const connectionString = sessionStorage.getItem("connectionString");
if (!connectionString) {
console.error("No connection string found in session storage");
return;
}
const encryptedToken = await fetchEncryptedToken(connectionString);
// TODO Duplicated from useTokenMetadata
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
const win = (window as unknown) as HostedExplorerChildFrame;
win.hostedConfig = {
authType: AuthType.EncryptedToken,
encryptedToken,
encryptedTokenMetadata,
};
const explorer = await configureHosted();
setExplorer(explorer); setExplorer(explorer);
} }
} }
@ -100,11 +85,104 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
return explorer; return explorer;
} }
async function configureFabric(): Promise<Explorer> {
let explorer: Explorer;
return new Promise<Explorer>((resolve) => {
window.addEventListener(
"message",
async (event) => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (!shouldProcessMessage(event)) {
return;
}
const data: FabricMessage = event.data?.data;
if (!data) {
return;
}
switch (data.type) {
case "initialize": {
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
const connectionString = data.connectionString ?? sessionStorage.getItem("connectionString");
if (!connectionString) {
console.error("No connection string found in session storage");
return undefined;
}
const encryptedToken = await fetchEncryptedToken(connectionString);
// TODO Duplicated from useTokenMetadata
const encryptedTokenMetadata = await fetchAccessData(encryptedToken);
const hostedConfig: EncryptedToken = {
authType: AuthType.EncryptedToken,
encryptedToken,
encryptedTokenMetadata,
};
explorer = await configureWithEncryptedToken(hostedConfig);
resolve(explorer);
break;
}
case "newContainer":
explorer.onNewCollectionClicked();
break;
case "openTab": {
// Expand database first
const databaseName = sessionStorage.getItem("openDatabaseName") ?? data.databaseName;
const database = useDatabases.getState().databases.find((db) => db.id() === databaseName);
if (database) {
await database.expandDatabase();
useDatabases.getState().updateDatabase(database);
useSelectedNode.getState().setSelectedNode(database);
let collectionResourceId = data.collectionName;
if (collectionResourceId === undefined) {
// Pick first collection if collectionName not specified in message
collectionResourceId = database.collections()[0]?.id();
}
if (collectionResourceId !== undefined) {
// Expand collection
const collection = database.collections().find((coll) => coll.id() === collectionResourceId);
collection.expandCollection();
useSelectedNode.getState().setSelectedNode(collection);
handleOpenAction(
{
actionType: ActionType.OpenCollectionTab,
databaseResourceId: databaseName,
collectionResourceId: data.collectionName,
tabKind: TabKind.SQLDocuments,
} as DataExplorerAction,
useDatabases.getState().databases,
explorer
);
}
}
break;
}
default:
console.error(`Unknown Fabric message type: ${JSON.stringify(data)}`);
break;
}
},
false
);
sendReadyMessage();
});
}
async function configureHosted(): Promise<Explorer> { async function configureHosted(): Promise<Explorer> {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
let explorer: Explorer; let explorer: Explorer;
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
explorer = configureHostedWithEncryptedToken(win.hostedConfig); explorer = configureWithEncryptedToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
explorer = configureHostedWithResourceToken(win.hostedConfig); explorer = configureHostedWithResourceToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) { } else if (win.hostedConfig.authType === AuthType.ConnectionString) {
@ -237,7 +315,7 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return explorer; return explorer;
} }
function configureHostedWithEncryptedToken(config: EncryptedToken): Explorer { function configureWithEncryptedToken(config: EncryptedToken): Explorer {
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,

View File

@ -3,6 +3,7 @@ import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels";
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab"; import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
import { Platform, configContext } from "./../ConfigContext";
interface TabsState { interface TabsState {
openedTabs: TabsBase[]; openedTabs: TabsBase[];
@ -37,11 +38,22 @@ export enum ReactTabKind {
QueryCopilot, QueryCopilot,
} }
// HACK: using this const when the configuration context is not initialized yet.
// Since Fabric is always setting the url param, use that instead of the regular config.
const isPlatformFabric = (() => {
const params = new URLSearchParams(window.location.search);
if (params.has("platform")) {
const platform = params.get("platform");
return platform === Platform.Fabric;
}
return false;
})();
export const useTabs: UseStore<TabsState> = create((set, get) => ({ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedTabs: [], openedTabs: [],
openedReactTabs: [ReactTabKind.Home], openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
activeTab: undefined, activeTab: undefined,
activeReactTab: ReactTabKind.Home, activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
networkSettingsWarning: "", networkSettingsWarning: "",
queryCopilotTabInitialInput: "", queryCopilotTabInitialInput: "",
isTabExecuting: false, isTabExecuting: false,
@ -92,7 +104,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
} }
return true; return true;
}); });
if (updatedTabs.length === 0) { if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
set({ activeTab: undefined, activeReactTab: ReactTabKind.Home }); set({ activeTab: undefined, activeReactTab: ReactTabKind.Home });
} }
@ -130,7 +142,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
} }
}); });
if (get().openedTabs.length === 0) { if (get().openedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
set({ activeTab: undefined, activeReactTab: ReactTabKind.Home }); set({ activeTab: undefined, activeReactTab: ReactTabKind.Home });
} }
} }