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:
parent
dfdb44bdc9
commit
d9e142d7a6
File diff suppressed because it is too large
Load Diff
|
@ -13,7 +13,7 @@
|
|||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
"@fluentui/react": "8.14.3",
|
||||
"@fluentui/react-components": "9.30.1",
|
||||
"@fluentui/react-components": "9.32.1",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
|
|
|
@ -61,6 +61,9 @@ let configContext: Readonly<ConfigContext> = {
|
|||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.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
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
};
|
|
@ -9,8 +9,11 @@ import {
|
|||
Tree,
|
||||
TreeItem,
|
||||
TreeItemLayout,
|
||||
TreeOpenChangeData,
|
||||
TreeOpenChangeEvent,
|
||||
} from "@fluentui/react-components";
|
||||
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
|
||||
import { tokens } from "@fluentui/react-theme";
|
||||
import * as React from "react";
|
||||
|
||||
export interface TreeNode2MenuItem {
|
||||
|
@ -27,7 +30,7 @@ export interface TreeNode2 {
|
|||
children?: TreeNode2[];
|
||||
contextMenu?: TreeNode2MenuItem[];
|
||||
iconSrc?: string;
|
||||
// isExpanded?: boolean;
|
||||
isExpanded?: boolean;
|
||||
className?: string;
|
||||
isAlphaSorted?: boolean;
|
||||
// data?: any; // Piece of data corresponding to this node
|
||||
|
@ -37,7 +40,7 @@ export interface TreeNode2 {
|
|||
isScrollable?: boolean;
|
||||
isSelected?: () => boolean;
|
||||
onClick?: () => void; // Only if a leaf, other click will expand/collapse
|
||||
onExpanded?: () => void;
|
||||
onExpanded?: () => Promise<void>;
|
||||
onCollapsed?: () => void;
|
||||
onContextMenuOpen?: () => void;
|
||||
}
|
||||
|
@ -46,7 +49,6 @@ export interface TreeNode2ComponentProps {
|
|||
node: TreeNode2;
|
||||
className?: string;
|
||||
treeNodeId: string;
|
||||
globalOpenIds: string[];
|
||||
}
|
||||
|
||||
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> = ({
|
||||
node,
|
||||
treeNodeId,
|
||||
globalOpenIds,
|
||||
}: TreeNode2ComponentProps): JSX.Element => {
|
||||
// const defaultOpenItems = node.isExpanded ? children?.map((child: TreeNode2) => child.label) : undefined;
|
||||
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 [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
|
||||
const getSortedChildren = (treeNode: TreeNode2): TreeNode2[] => {
|
||||
if (!treeNode || !treeNode.children) {
|
||||
|
@ -95,8 +85,24 @@ export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
|
|||
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 (
|
||||
<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
|
||||
className={node.className}
|
||||
actions={
|
||||
|
@ -117,22 +123,21 @@ export const TreeNode2Component: React.FC<TreeNode2ComponentProps> = ({
|
|||
</Menu>
|
||||
)
|
||||
}
|
||||
expandIcon={node.isLoading ? <Spinner size="extra-tiny" /> : undefined}
|
||||
expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
|
||||
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
|
||||
style={{
|
||||
backgroundColor: node.isSelected && node.isSelected() ? tokens.colorNeutralBackground1Selected : undefined,
|
||||
}}
|
||||
>
|
||||
<span onClick={() => node.onClick?.()}>{node.label}</span>
|
||||
</TreeItemLayout>
|
||||
{!node.isLoading && node.children?.length > 0 && (
|
||||
<Tree
|
||||
// defaultOpenItems={defaultOpenItems}
|
||||
style={{ overflow: node.isScrollable ? "auto" : undefined }}
|
||||
>
|
||||
<Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
|
||||
{getSortedChildren(node).map((childNode: TreeNode2) => (
|
||||
<TreeNode2Component
|
||||
key={childNode.label}
|
||||
node={childNode}
|
||||
treeNodeId={`${treeNodeId}/${childNode.label}`}
|
||||
globalOpenIds={globalOpenIds}
|
||||
/>
|
||||
))}
|
||||
</Tree>
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
TreeOpenChangeEvent,
|
||||
createLightTheme,
|
||||
} 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 * as React from "react";
|
||||
import shallow from "zustand/shallow";
|
||||
|
@ -69,16 +69,45 @@ export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: Resour
|
|||
);
|
||||
// const { activeTab } = useTabs();
|
||||
const databaseTreeNodes = useDatabaseTreeNodes(container, isNotebookEnabled);
|
||||
const dataNodeTree = {
|
||||
const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([DATA_TREE_LABEL]);
|
||||
|
||||
const dataNodeTree: TreeNode2 = {
|
||||
id: "data",
|
||||
label: DATA_TREE_LABEL,
|
||||
isExpanded: true,
|
||||
className: "accordionItemHeader",
|
||||
children: databaseTreeNodes,
|
||||
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);
|
||||
|
||||
|
@ -93,13 +122,7 @@ export const ResourceTree2: React.FC<ResourceTreeProps> = ({ container }: Resour
|
|||
style={{ height: "100%", width: "290px" }}
|
||||
>
|
||||
{[dataNodeTree].map((node) => (
|
||||
<TreeNode2Component
|
||||
key={node.label}
|
||||
className="dataResourceTree"
|
||||
node={node}
|
||||
treeNodeId={node.label}
|
||||
globalOpenIds={[...openItems].map((item) => item.toString())}
|
||||
/>
|
||||
<TreeNode2Component key={node.label} className="dataResourceTree" node={node} treeNodeId={node.label} />
|
||||
))}
|
||||
</Tree>
|
||||
</FluentProvider>
|
||||
|
|
|
@ -45,7 +45,7 @@ export const buildCollectionNode = (
|
|||
// push to most recent
|
||||
mostRecentActivity.collectionWasOpened(userContext.databaseAccount?.id, collection);
|
||||
},
|
||||
onExpanded: () => {
|
||||
onExpanded: async () => {
|
||||
// Rewritten version of expandCollapseCollection
|
||||
useSelectedNode.getState().setSelectedNode(collection);
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
|
@ -53,9 +53,16 @@ export const buildCollectionNode = (
|
|||
(tab: TabsBase) =>
|
||||
tab.collection?.id() === collection.id() && tab.collection.databaseId === collection.databaseId
|
||||
);
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
},
|
||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(collection.databaseId, collection.id()),
|
||||
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(collection),
|
||||
onCollapsed: () => {
|
||||
collection.collapseCollection();
|
||||
// useCommandBar.getState().setContextButtons([]);
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
},
|
||||
isExpanded: collection.isCollectionExpanded(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -25,15 +25,22 @@ export const useDatabaseTreeNodes = (container: Explorer, isNotebookEnabled: boo
|
|||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(container, database.id()),
|
||||
onExpanded: async () => {
|
||||
useSelectedNode.getState().setSelectedNode(database);
|
||||
if (databaseNode.children?.length === 0) {
|
||||
if (!databaseNode.children || databaseNode.children?.length === 0) {
|
||||
databaseNode.isLoading = true;
|
||||
}
|
||||
await database.expandDatabase();
|
||||
databaseNode.isLoading = false;
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
refreshActiveTab((tab: TabsBase) => tab.collection?.databaseId === database.id());
|
||||
useDatabases.getState().updateDatabase(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) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { createUri } from "Common/UrlUtility";
|
||||
import { FabricMessage } from "Contracts/FabricContract";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { fetchEncryptedToken } from "Platform/Hosted/Components/ConnectExplorer";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
import { fetchAccessData } from "hooks/usePortalAccessToken";
|
||||
|
@ -10,7 +12,7 @@ import { AccountKind, Flights } from "../Common/Constants";
|
|||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import { sendMessage, sendReadyMessage } from "../Common/MessageHandler";
|
||||
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 { DataExplorerInputsFrame } from "../Contracts/ViewModels";
|
||||
import { handleOpenAction } from "../Explorer/OpenActions/OpenActions";
|
||||
|
@ -63,24 +65,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||
const explorer = await configurePortal();
|
||||
setExplorer(explorer);
|
||||
} else if (platform === Platform.Fabric) {
|
||||
// TODO For now, retrieve info from session storage. Replace with info injected into Data Explorer
|
||||
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();
|
||||
const explorer = await configureFabric();
|
||||
setExplorer(explorer);
|
||||
}
|
||||
}
|
||||
|
@ -100,11 +85,104 @@ export function useKnockoutExplorer(platform: Platform): 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> {
|
||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||
let explorer: Explorer;
|
||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||
explorer = configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
explorer = configureWithEncryptedToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||
explorer = configureHostedWithResourceToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||
|
@ -237,7 +315,7 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
|
|||
return explorer;
|
||||
}
|
||||
|
||||
function configureHostedWithEncryptedToken(config: EncryptedToken): Explorer {
|
||||
function configureWithEncryptedToken(config: EncryptedToken): Explorer {
|
||||
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
|
||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
|
||||
interface TabsState {
|
||||
openedTabs: TabsBase[];
|
||||
|
@ -37,11 +38,22 @@ export enum ReactTabKind {
|
|||
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) => ({
|
||||
openedTabs: [],
|
||||
openedReactTabs: [ReactTabKind.Home],
|
||||
openedReactTabs: !isPlatformFabric ? [ReactTabKind.Home] : [],
|
||||
activeTab: undefined,
|
||||
activeReactTab: ReactTabKind.Home,
|
||||
activeReactTab: !isPlatformFabric ? ReactTabKind.Home : undefined,
|
||||
networkSettingsWarning: "",
|
||||
queryCopilotTabInitialInput: "",
|
||||
isTabExecuting: false,
|
||||
|
@ -92,7 +104,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||
}
|
||||
return true;
|
||||
});
|
||||
if (updatedTabs.length === 0) {
|
||||
if (updatedTabs.length === 0 && configContext.platform !== Platform.Fabric) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue