-
-
-
- When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.
-
-
-
-
- Would you like to publish and share "SampleNotebook" to the gallery?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Preview
-
-
-
-
-
-
-
-`;
diff --git a/src/Explorer/Panes/PublishNotebookPane/styled.less b/src/Explorer/Panes/PublishNotebookPane/styled.less
deleted file mode 100644
index e1170b670..000000000
--- a/src/Explorer/Panes/PublishNotebookPane/styled.less
+++ /dev/null
@@ -1,6 +0,0 @@
-.publishNotebookPanelContent {
- display: flex;
- flex-direction: column;
- flex: 1;
- overflow-y: auto;
-}
\ No newline at end of file
diff --git a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts
index 41a1d49e4..f786e9ee4 100644
--- a/src/Explorer/Tables/DataTable/DataTableBindingManager.ts
+++ b/src/Explorer/Tables/DataTable/DataTableBindingManager.ts
@@ -93,7 +93,7 @@ function createDataTable(
for (var i = 0; i < tableEntityListViewModel.headers.length; i++) {
jsonColTable.push({
- sTitle: tableEntityListViewModel.headers[i],
+ sTitle: Utilities.htmlEncode(tableEntityListViewModel.headers[i]),
data: tableEntityListViewModel.headers[i],
aTargets: [i],
mRender: bindColumn,
diff --git a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
index 1c69e0efc..dc041ec91 100644
--- a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
+++ b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
@@ -1,3 +1,4 @@
+import { stringifyError } from "Common/stringifyError";
import * as DataTables from "datatables.net";
import * as ko from "knockout";
import Q from "q";
@@ -37,7 +38,7 @@ function parseError(err: any): ErrorDataModel[] {
try {
return _parse(err);
} catch (e) {
- return [{ message: JSON.stringify(err) }];
+ return [{ message: stringifyError(err) }];
}
}
diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts
index 631e19e88..ed9992af5 100644
--- a/src/Explorer/Tables/TableDataClient.ts
+++ b/src/Explorer/Tables/TableDataClient.ts
@@ -1,4 +1,5 @@
import { FeedOptions } from "@azure/cosmos";
+import { stringifyError } from "Common/stringifyError";
import * as ko from "knockout";
import Q from "q";
import { AuthType } from "../../AuthType";
@@ -172,7 +173,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity);
},
(error) => {
- const errorText = error.responseJSON?.message ?? JSON.stringify(error);
+ const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText);
},
@@ -361,7 +362,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
- const errorText = error.responseJSON?.message ?? JSON.stringify(error);
+ const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(
errorText,
"CreateKeyspaceCassandra",
@@ -400,7 +401,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve();
},
(error) => {
- const errorText = error.responseJSON?.message ?? JSON.stringify(error);
+ const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(
errorText,
"CreateTableCassandra",
@@ -450,7 +451,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data);
},
(error: any) => {
- const errorText = error.responseJSON?.message ?? JSON.stringify(error);
+ const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText);
},
@@ -492,7 +493,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns);
},
(error: any) => {
- const errorText = error.responseJSON?.message ?? JSON.stringify(error);
+ const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText);
},
diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.duplicateTab.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.duplicateTab.test.tsx
new file mode 100644
index 000000000..673d6e828
--- /dev/null
+++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.duplicateTab.test.tsx
@@ -0,0 +1,96 @@
+import { useTabs } from "hooks/useTabs";
+import * as ko from "knockout";
+import * as ViewModels from "../../../Contracts/ViewModels";
+import { DocumentsTabV2 } from "./DocumentsTabV2";
+
+jest.mock("hooks/useTabs", () => ({
+ useTabs: {
+ getState: jest.fn(),
+ },
+}));
+
+jest.mock("UserContext", () => ({
+ userContext: { apiType: "SQL" },
+}));
+
+jest.mock("Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
+ useCommandBar: { getState: jest.fn(() => ({ setContextButtons: jest.fn() })) },
+}));
+
+jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
+ EditorReact: () => null,
+}));
+
+const mockCollection = {
+ id: ko.observable("testContainer"),
+ databaseId: "testDb",
+ partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
+ selectedSubnodeKind: jest.fn(),
+ container: {},
+} as unknown as ViewModels.Collection;
+
+const buildTab = () =>
+ new DocumentsTabV2({
+ partitionKey: mockCollection.partitionKey,
+ documentIds: ko.observableArray([]),
+ tabKind: ViewModels.CollectionTabKind.Documents,
+ title: "Items",
+ collection: mockCollection,
+ node: mockCollection,
+ tabPath: "testDb>testContainer>Documents",
+ });
+
+describe("DocumentsTabV2.duplicateTab", () => {
+ let activateNewTab: jest.Mock;
+
+ beforeEach(() => {
+ activateNewTab = jest.fn();
+ (useTabs.getState as jest.Mock).mockReturnValue({ activateNewTab });
+ });
+
+ afterEach(() => jest.clearAllMocks());
+
+ it("calls activateNewTab with a new DocumentsTabV2 instance", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ expect(activateNewTab).toHaveBeenCalledTimes(1);
+ const newTab = activateNewTab.mock.calls[0][0];
+ expect(newTab).toBeInstanceOf(DocumentsTabV2);
+ });
+
+ it("creates a duplicate with the same collection", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
+ expect(newTab.collection).toBe(mockCollection);
+ });
+
+ it("creates a duplicate with the same partitionKey", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
+ expect(newTab.partitionKey).toEqual(mockCollection.partitionKey);
+ });
+
+ it("creates a distinct tab instance", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0];
+ expect(newTab).not.toBe(tab);
+ });
+
+ it("preserves the raw title (not the display title) to avoid double-prefixing", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0] as DocumentsTabV2;
+ // The original tabTitle() is "testCโฆItems" (collection "testContainer" is > 8 chars so it's truncated).
+ // If duplicateTab() incorrectly used tabTitle() as the new title, the duplicate's tabTitle()
+ // would double-prefix to "testCโฆtestCโฆItems". Using the raw title "Items" keeps it "testCโฆItems".
+ expect(newTab.tabTitle()).toBe("testC\u2026Items");
+ });
+});
diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx
index 8f2c252d0..5d9f2bb37 100644
--- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx
+++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx
@@ -49,6 +49,8 @@ import { userContext } from "UserContext";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import { Allotment } from "allotment";
import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
+import { useTabs } from "hooks/useTabs";
+import ko from "knockout";
import React, { KeyboardEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { format } from "react-string-format";
import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
@@ -176,6 +178,25 @@ export class DocumentsTabV2 extends TabsBase {
};
}
+ public canDuplicate(): boolean {
+ return true;
+ }
+
+ public duplicateTab(): void {
+ const newTab = new DocumentsTabV2({
+ partitionKey: this.partitionKey,
+ documentIds: ko.observableArray([]),
+ tabKind: ViewModels.CollectionTabKind.Documents,
+ title: this.title,
+ collection: this.collection,
+ node: this.collection,
+ tabPath: `${this.collection.databaseId}>${this.collection.id()}>Documents`,
+ isPreferredApiMongoDB: userContext.apiType === "Mongo",
+ resourceTokenPartitionKey: this.resourceTokenPartitionKey,
+ });
+ useTabs.getState().activateNewTab(newTab);
+ }
+
public render(): JSX.Element {
return (
;
- }
-
- public getContainer(): Explorer {
- return this.props.container;
- }
-}
diff --git a/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx b/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx
index 22e50e1c8..80ce4c4f8 100644
--- a/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx
+++ b/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx
@@ -2,6 +2,7 @@ import { ActionType, TabKind } from "Contracts/ActionContracts";
import React from "react";
import MongoUtility from "../../../Common/MongoUtility";
import * as ViewModels from "../../../Contracts/ViewModels";
+import { useTabs } from "../../../hooks/useTabs";
import Explorer from "../../Explorer";
import { NewQueryTab } from "../QueryTab/QueryTab";
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../QueryTab/QueryTabComponent";
@@ -67,6 +68,30 @@ export class NewMongoQueryTab extends NewQueryTab {
return MongoUtility.tojson(value, undefined, false);
}
+ public canDuplicate(): boolean {
+ return true;
+ }
+
+ public duplicateTab(): void {
+ const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
+ const queryText = this.iTabAccessor?.onSaveClickEvent() ?? this.persistedState?.query?.text ?? "";
+ const newTab = new NewMongoQueryTab(
+ {
+ tabKind: ViewModels.CollectionTabKind.Query,
+ title: `Query ${id}`,
+ tabPath: "",
+ collection: this.collection,
+ node: this.collection,
+ queryText,
+ partitionKey: this.partitionKey,
+ splitterDirection: this.persistedState?.splitterDirection,
+ queryViewSizePercent: this.persistedState?.queryViewSizePercent,
+ },
+ this.mongoQueryTabProps,
+ );
+ useTabs.getState().activateNewTab(newTab);
+ }
+
public render(): JSX.Element {
return ;
}
diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts
index 92e0e6958..b54efc00e 100644
--- a/src/Explorer/Tabs/NotebookV2Tab.ts
+++ b/src/Explorer/Tabs/NotebookV2Tab.ts
@@ -1,7 +1,6 @@
import { stringifyNotebook, toJS } from "@nteract/commutable";
import * as ko from "knockout";
import * as Q from "q";
-import { userContext } from "UserContext";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
@@ -12,8 +11,7 @@ import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg";
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
-import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore";
-import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
+import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
@@ -21,9 +19,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu
import { useDialog } from "../Controls/Dialog";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
-import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
-import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
@@ -97,7 +93,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
const saveLabel = "Save";
const copyToLabel = "Copy to ...";
- const publishLabel = "Publish to gallery";
const kernelLabel = "No Kernel";
const runLabel = "Run";
const runActiveCellLabel = "Run Active Cell";
@@ -130,17 +125,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
});
}
- if (userContext.features.publicGallery) {
- saveButtonChildren.push({
- iconName: "PublishContent",
- onCommandClick: async () => await this.publishToGallery(),
- commandButtonLabel: publishLabel,
- hasPopup: false,
- disabled: false,
- ariaLabel: publishLabel,
- });
- }
-
let buttons: CommandButtonComponentProps[] = [
{
iconSrc: SaveIcon,
@@ -383,40 +367,6 @@ export default class NotebookTabV2 extends NotebookTabBase {
);
}
- private publishToGallery = async () => {
- TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
- source: Source.CommandBarMenu,
- });
-
- const notebookReduxStore = NotebookTabV2.clientManager.getStore();
- const unsubscribe = notebookReduxStore.subscribe(() => {
- const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb;
- useNotebookSnapshotStore.setState({
- snapshot: cdbState.notebookSnapshot?.imageSrc,
- error: cdbState.notebookSnapshotError,
- });
- });
-
- const notebookContent = this.notebookComponentAdapter.getContent();
- const notebookContentRef = this.notebookComponentAdapter.contentRef;
- const onPanelClose = (): void => {
- unsubscribe();
- useNotebookSnapshotStore.setState({
- snapshot: undefined,
- error: undefined,
- });
- notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined));
- };
-
- await this.container.publishNotebook(
- notebookContent.name,
- notebookContent.content,
- notebookContentRef,
- (request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)),
- onPanelClose,
- );
- };
-
private copyNotebook = () => {
const notebookContent = this.notebookComponentAdapter.getContent();
let content: string;
diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.duplicateTab.test.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.duplicateTab.test.tsx
new file mode 100644
index 000000000..59b712f45
--- /dev/null
+++ b/src/Explorer/Tabs/QueryTab/QueryTab.duplicateTab.test.tsx
@@ -0,0 +1,104 @@
+import { useTabs } from "hooks/useTabs";
+import * as ko from "knockout";
+import * as ViewModels from "../../../Contracts/ViewModels";
+import { IQueryTabProps, NewQueryTab } from "./QueryTab";
+
+jest.mock("hooks/useTabs", () => ({
+ useTabs: {
+ getState: jest.fn(),
+ },
+}));
+
+jest.mock("Explorer/Menus/CommandBar/CommandBarComponentAdapter", () => ({
+ useCommandBar: { getState: jest.fn(() => ({ setContextButtons: jest.fn() })) },
+}));
+
+jest.mock("Shared/AppStatePersistenceUtility", () => ({
+ loadState: jest.fn(),
+ AppStateComponentNames: {},
+ readSubComponentState: jest.fn(),
+}));
+
+jest.mock("Common/MessageHandler", () => ({
+ sendMessage: jest.fn(),
+}));
+
+const mockCollection = {
+ id: ko.observable("testContainer"),
+ databaseId: "testDb",
+ partitionKey: { paths: ["/pk"], kind: "Hash", version: 2 },
+ selectedSubnodeKind: jest.fn(),
+ container: {},
+} as unknown as ViewModels.Collection;
+
+const mockProps = { container: {} as IQueryTabProps["container"] };
+
+const buildTab = (queryText = "SELECT * FROM c") =>
+ new NewQueryTab(
+ {
+ tabKind: ViewModels.CollectionTabKind.Query,
+ title: "Query 1",
+ tabPath: "",
+ collection: mockCollection,
+ node: mockCollection,
+ queryText,
+ partitionKey: mockCollection.partitionKey,
+ },
+ mockProps,
+ );
+
+describe("NewQueryTab.duplicateTab", () => {
+ let activateNewTab: jest.Mock;
+ let getTabs: jest.Mock;
+
+ beforeEach(() => {
+ activateNewTab = jest.fn();
+ getTabs = jest.fn().mockReturnValue([]);
+ (useTabs.getState as jest.Mock).mockReturnValue({ activateNewTab, getTabs });
+ });
+
+ afterEach(() => jest.clearAllMocks());
+
+ it("calls activateNewTab with a new NewQueryTab instance", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ expect(activateNewTab).toHaveBeenCalledTimes(1);
+ const newTab = activateNewTab.mock.calls[0][0];
+ expect(newTab).toBeInstanceOf(NewQueryTab);
+ });
+
+ it("preserves the current query text in the duplicate", () => {
+ const queryText = "SELECT * FROM c WHERE c.id = '123'";
+ const tab = buildTab(queryText);
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
+ expect(newTab.iQueryTabComponentProps.queryText).toBe(queryText);
+ });
+
+ it("creates a duplicate with the same collection", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
+ expect(newTab.collection).toBe(mockCollection);
+ });
+
+ it("creates a distinct tab instance", () => {
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0];
+ expect(newTab).not.toBe(tab);
+ });
+
+ it("assigns an auto-incremented title based on existing query tabs", () => {
+ getTabs.mockReturnValue([{}, {}]); // 2 existing tabs โ new title = "Query 3"
+ const tab = buildTab();
+ tab.duplicateTab();
+
+ const newTab = activateNewTab.mock.calls[0][0] as NewQueryTab;
+ expect(newTab.tabTitle()).toContain("Query 3");
+ });
+});
diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx
index e504d601e..61013a113 100644
--- a/src/Explorer/Tabs/QueryTab/QueryTab.tsx
+++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx
@@ -4,7 +4,8 @@ import { MessageTypes } from "Contracts/MessageTypes";
import React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import type { QueryTabOptions } from "../../../Contracts/ViewModels";
-import { useTabs } from "../../../hooks/useTabs";
+import * as ViewModels from "../../../Contracts/ViewModels";
+import { useTabs } from "hooks/useTabs";
import Explorer from "../../Explorer";
import { IQueryTabComponentProps, ITabAccessor, QueryTabComponent } from "../../Tabs/QueryTab/QueryTabComponent";
import TabsBase from "../TabsBase";
@@ -72,6 +73,30 @@ export class NewQueryTab extends TabsBase {
});
}
+ public canDuplicate(): boolean {
+ return true;
+ }
+
+ public duplicateTab(): void {
+ const queryText = this.iTabAccessor?.onSaveClickEvent() ?? this.persistedState?.query?.text ?? "";
+ const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Query).length + 1;
+ const newTab = new NewQueryTab(
+ {
+ tabKind: ViewModels.CollectionTabKind.Query,
+ title: `Query ${id}`,
+ tabPath: "",
+ collection: this.collection,
+ node: this.collection,
+ queryText,
+ partitionKey: this.partitionKey,
+ splitterDirection: this.persistedState?.splitterDirection,
+ queryViewSizePercent: this.persistedState?.queryViewSizePercent,
+ },
+ this.props,
+ );
+ useTabs.getState().activateNewTab(newTab);
+ }
+
public render(): JSX.Element {
return ;
}
diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx
index e0fbccd76..54fefedd1 100644
--- a/src/Explorer/Tabs/Tabs.tsx
+++ b/src/Explorer/Tabs/Tabs.tsx
@@ -1,4 +1,5 @@
import { Spinner, SpinnerSize, TooltipHost } from "@fluentui/react";
+import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from "@fluentui/react-components";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -10,6 +11,7 @@ import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
+import { Keys, t } from "Localization";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import { useTeachingBubble } from "hooks/useTeachingBubble";
@@ -85,8 +87,9 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
focusTab.current.focus();
}
}, [active]);
- return (
+ const liElement = (
);
+
+ if (!tab?.canDuplicate()) {
+ return liElement;
+ }
+
+ return (
+
+ );
}
const onKeyPressReactTabClose = (e: KeyboardEvent, tabKind: ReactTabKind): void => {
diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts
index 2602b672d..40e79b531 100644
--- a/src/Explorer/Tabs/TabsBase.ts
+++ b/src/Explorer/Tabs/TabsBase.ts
@@ -64,6 +64,14 @@ export default class TabsBase extends WaitsForTemplateViewModel {
public getPersistedState = (): OpenTab | null => this.persistedState;
public triggerPersistState: () => void = undefined;
+ public canDuplicate(): boolean {
+ return false;
+ }
+
+ public duplicateTab(): void {
+ // Subclasses override this to support tab duplication
+ }
+
public onCloseTabButtonClick(): void {
useTabs.getState().closeTab(this);
TelemetryProcessor.trace(Action.Tab, ActionModifiers.Close, {
diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx
index 819c73d6c..2d19bc8e9 100644
--- a/src/Explorer/Tree/ResourceTreeAdapter.tsx
+++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx
@@ -10,7 +10,6 @@ import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
-import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
@@ -18,7 +17,7 @@ import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtili
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
-import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
+import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
@@ -49,7 +48,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
public parameters: ko.Observable;
- public galleryContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem;
public gitHubNotebooksContentRoot: NotebookContentItem;
@@ -102,11 +100,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
public async initialize(): Promise {
const refreshTasks: Promise[] = [];
- this.galleryContentRoot = {
- name: "Gallery",
- path: "Gallery",
- type: NotebookContentItemType.File,
- };
this.myNotebooksContentRoot = {
name: useNotebook.getState().notebookFolderName,
path: useNotebook.getState().notebookBasePath,
@@ -538,20 +531,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
];
if (item.type === NotebookContentItemType.Notebook) {
- items.push({
- label: "Publish to gallery",
- iconSrc: PublishIcon,
- onClick: async () => {
- TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
- source: Source.ResourceTreeMenu,
- });
-
- const content = await this.container.readFile(item);
- if (content) {
- await this.container.publishNotebook(item.name, content);
- }
- },
- });
+ // Additional notebook-specific context menu items can be added here
}
// "Copy to ..." isn't needed if github locations are not available
diff --git a/src/GalleryViewer/GalleryViewer.less b/src/GalleryViewer/GalleryViewer.less
deleted file mode 100644
index 2eab230e9..000000000
--- a/src/GalleryViewer/GalleryViewer.less
+++ /dev/null
@@ -1,5 +0,0 @@
-@import "../../less/Common/Constants";
-
-.standalone-gallery-root {
- background: @GalleryBackgroundColor;
-}
\ No newline at end of file
diff --git a/src/GalleryViewer/GalleryViewer.tsx b/src/GalleryViewer/GalleryViewer.tsx
deleted file mode 100644
index 2026415af..000000000
--- a/src/GalleryViewer/GalleryViewer.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { initializeIcons, Link, Text } from "@fluentui/react";
-import "bootstrap/dist/css/bootstrap.css";
-import * as React from "react";
-import * as ReactDOM from "react-dom";
-import { userContext } from "UserContext";
-import { initializeConfiguration } from "../ConfigContext";
-import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
-import {
- GalleryAndNotebookViewerComponent,
- GalleryAndNotebookViewerComponentProps,
-} from "../Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
-import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
-import { JunoClient } from "../Juno/JunoClient";
-import * as GalleryUtils from "../Utils/GalleryUtils";
-import "./GalleryViewer.less";
-
-const enableNotebooksUrl = "https://aka.ms/cosmos-enable-notebooks";
-const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
-
-const onInit = async () => {
- const dataExplorerUrl = new URL("./", window.location.href).href;
-
- initializeIcons();
- await initializeConfiguration();
- const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
-
- const props: GalleryAndNotebookViewerComponentProps = {
- junoClient: new JunoClient(),
- selectedTab:
- galleryViewerProps.selectedTab ||
- (userContext.features.publicGallery ? GalleryTab.PublicGallery : GalleryTab.OfficialSamples),
- sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
- searchText: galleryViewerProps.searchText,
- };
-
- const element = (
-
-
-
-
-
-
-
- Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best
- practices, and how to get started with Azure Cosmos DB.
-
-
- If {`you'd`} like to run or edit the notebook in your own Azure Cosmos DB account,{" "}
- sign in and select an account with{" "}
- notebooks enabled. From there, you can download the sample to your
- account. If you {`don't`} have an account yet, you can{" "}
- create one from the Azure portal.
-
-