mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 09:20:16 +00:00
Initial Move from Azure DevOps to GitHub
This commit is contained in:
14
src/Explorer/Menus/CommandBar/CommandBarComponent.less
Normal file
14
src/Explorer/Menus/CommandBar/CommandBarComponent.less
Normal file
@@ -0,0 +1,14 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.dividerContainer {
|
||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||
.flex-display();
|
||||
span {
|
||||
border-left: @ButtonBorderWidth solid @BaseMediumHigh;
|
||||
margin: 0 10px 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.commandBarContainer {
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
}
|
||||
103
src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
Normal file
103
src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* This adapter is responsible to render the React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
|
||||
export class CommandBarComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
public container: ViewModels.Explorer;
|
||||
private tabsButtons: ViewModels.NavbarButtonConfig[];
|
||||
|
||||
constructor(container: ViewModels.Explorer) {
|
||||
this.container = container;
|
||||
this.tabsButtons = [];
|
||||
|
||||
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
|
||||
const toWatch = [
|
||||
container.isPreferredApiTable,
|
||||
container.isPreferredApiMongoDB,
|
||||
container.isPreferredApiDocumentDB,
|
||||
container.isPreferredApiCassandra,
|
||||
container.isPreferredApiGraph,
|
||||
container.deleteCollectionText,
|
||||
container.deleteDatabaseText,
|
||||
container.addCollectionText,
|
||||
container.addDatabaseText,
|
||||
container.isDatabaseNodeOrNoneSelected,
|
||||
container.isDatabaseNodeSelected,
|
||||
container.isNoneSelected,
|
||||
container.isResourceTokenCollectionNodeSelected,
|
||||
container.isHostedDataExplorerEnabled,
|
||||
container.isSynapseLinkUpdating,
|
||||
container.databaseAccount,
|
||||
container.isNotebookTabActive
|
||||
];
|
||||
|
||||
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
|
||||
this.parameters = ko.observable(Date.now());
|
||||
}
|
||||
|
||||
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
|
||||
this.tabsButtons = buttons;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
|
||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(this.container);
|
||||
const contextButtons = (this.tabsButtons || []).concat(
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(this.container)
|
||||
);
|
||||
const controlButtons = CommandBarComponentButtonFactory.createControlCommandBarButtons(this.container);
|
||||
|
||||
const uiFabricStaticButtons = CommandBarUtil.convertButton(staticButtons, backgroundColor);
|
||||
if (this.tabsButtons && this.tabsButtons.length > 0) {
|
||||
uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
}
|
||||
|
||||
const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, backgroundColor);
|
||||
|
||||
if (uiFabricTabsButtons.length > 0) {
|
||||
uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider"));
|
||||
}
|
||||
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (this.container && this.container.isNotebookTabActive()) {
|
||||
uiFabricControlButtons.unshift(
|
||||
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="commandBarContainer">
|
||||
<CommandBar
|
||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||
farItems={uiFabricControlButtons}
|
||||
styles={{
|
||||
root: { backgroundColor: backgroundColor }
|
||||
}}
|
||||
overflowButtonProps={{ ariaLabel: "More commands" }}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private triggerRender() {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||
import { ExplorerStub } from "../../OpenActionsStubs";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
|
||||
describe("CommandBarComponentButtonFactory tests", () => {
|
||||
let mockExplorer: ViewModels.Explorer;
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = new ExplorerStub();
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isAuthWithResourceToken = ko.observable(false);
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
|
||||
it("Notebooks is already enabled - button should be hidden", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find(button => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Account is running on one of the national clouds - button should be hidden", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find(button => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled but is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find(button => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(false);
|
||||
expect(enableNotebookBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const enableNotebookBtn = buttons.find(button => button.commandButtonLabel === enableNotebookBtnLabel);
|
||||
expect(enableNotebookBtn).toBeDefined();
|
||||
expect(enableNotebookBtn.disabled).toBe(true);
|
||||
expect(enableNotebookBtn.tooltipText).toBe(
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Mongo Shell button", () => {
|
||||
const openMongoShellBtnLabel = "Open Mongo Shell";
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = new ExplorerStub();
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isAuthWithResourceToken = ko.observable(false);
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
});
|
||||
|
||||
it("Mongo Api not available - button should be hidden", () => {
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find(button => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Running on a national cloud - button should be hidden", () => {
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find(button => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find(button => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(true);
|
||||
expect(openMongoShellBtn.tooltipText).toBe(
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
);
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find(button => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find(button => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find(button => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeDefined();
|
||||
expect(openMongoShellBtn.disabled).toBe(false);
|
||||
expect(openMongoShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Cassandra Shell button", () => {
|
||||
const openCassandraShellBtnLabel = "Open Cassandra Shell";
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = new ExplorerStub();
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isAuthWithResourceToken = ko.observable(false);
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
});
|
||||
|
||||
it("Cassandra Api not available - button should be hidden", () => {
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find(button => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Running on a national cloud - button should be hidden", () => {
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find(button => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find(button => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(true);
|
||||
expect(openCassandraShellBtn.tooltipText).toBe(
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
|
||||
);
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find(button => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find(button => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openCassandraShellBtn = buttons.find(button => button.commandButtonLabel === openCassandraShellBtnLabel);
|
||||
expect(openCassandraShellBtn).toBeDefined();
|
||||
expect(openCassandraShellBtn.disabled).toBe(false);
|
||||
expect(openCassandraShellBtn.tooltipText).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GitHub buttons", () => {
|
||||
const connectToGitHubBtnLabel = "Connect to GitHub";
|
||||
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = new ExplorerStub();
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isAuthWithResourceToken = ko.observable(false);
|
||||
mockExplorer.isPreferredApiTable = ko.computed(() => true);
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
mockExplorer.isGalleryEnabled = ko.computed<boolean>(() => false);
|
||||
mockExplorer.gitHubOAuthService = new GitHubOAuthService(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is not logged in - connect to github button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const connectToGitHubBtn = buttons.find(button => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is enabled and GitHubOAuthService is logged in - manage github settings button should be visible", () => {
|
||||
mockExplorer.isNotebookEnabled = ko.observable(true);
|
||||
mockExplorer.gitHubOAuthService.isLoggedIn = jest.fn().mockReturnValue(true);
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
button => button.commandButtonLabel === manageGitHubSettingsBtnLabel
|
||||
);
|
||||
expect(manageGitHubSettingsBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Notebooks is not enabled - connect to github and manage github settings buttons should be hidden", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
|
||||
const connectToGitHubBtn = buttons.find(button => button.commandButtonLabel === connectToGitHubBtnLabel);
|
||||
expect(connectToGitHubBtn).toBeUndefined();
|
||||
|
||||
const manageGitHubSettingsBtn = buttons.find(
|
||||
button => button.commandButtonLabel === manageGitHubSettingsBtnLabel
|
||||
);
|
||||
expect(manageGitHubSettingsBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resource token", () => {
|
||||
beforeAll(() => {
|
||||
mockExplorer = new ExplorerStub();
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
mockExplorer.isAuthWithResourceToken = ko.observable(true);
|
||||
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
|
||||
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
|
||||
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
|
||||
});
|
||||
|
||||
it("should only show New SQL Query and Open Query buttons", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
expect(buttons.length).toBe(2);
|
||||
expect(buttons[0].commandButtonLabel).toBe("New SQL Query");
|
||||
expect(buttons[0].disabled).toBe(false);
|
||||
expect(buttons[1].commandButtonLabel).toBe("Open Query");
|
||||
expect(buttons[1].disabled).toBe(false);
|
||||
expect(buttons[1].children).toBeDefined();
|
||||
expect(buttons[1].children.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,727 @@
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { PlatformType } from "../../../PlatformType";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
import ApacheSparkIcon from "../../../../images/notebook/Apache-spark.svg";
|
||||
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import DeleteIcon from "../../../../images/delete.svg";
|
||||
import EditIcon from "../../../../images/edit.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||
import AddUdfIcon from "../../../../images/AddUdf.svg";
|
||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||
import ScaleIcon from "../../../../images/Scale_15x15.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import LibraryManageIcon from "../../../../images/notebook/Spark-library-manage.svg";
|
||||
import GalleryIcon from "../../../../images/GalleryIcon.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
import { config, Platform } from "../../../Config";
|
||||
|
||||
export class CommandBarComponentButtonFactory {
|
||||
private static counter: number = 0;
|
||||
|
||||
public static createStaticCommandBarButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
if (container.isAuthWithResourceToken()) {
|
||||
return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container);
|
||||
}
|
||||
|
||||
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [newCollectionBtn];
|
||||
|
||||
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
|
||||
if (addSynapseLink) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createDivider());
|
||||
buttons.push(addSynapseLink);
|
||||
}
|
||||
|
||||
if (!container.isPreferredApiTable()) {
|
||||
newCollectionBtn.children = [CommandBarComponentButtonFactory.createNewCollectionGroup(container)];
|
||||
const newDatabaseBtn = CommandBarComponentButtonFactory.createNewDatabase(container);
|
||||
newCollectionBtn.children.push(newDatabaseBtn);
|
||||
}
|
||||
|
||||
buttons.push(CommandBarComponentButtonFactory.createDivider());
|
||||
|
||||
if (container.isNotebookEnabled()) {
|
||||
const newNotebookButton = CommandBarComponentButtonFactory.createNewNotebookButton(container);
|
||||
newNotebookButton.children = [
|
||||
CommandBarComponentButtonFactory.createNewNotebookButton(container),
|
||||
CommandBarComponentButtonFactory.createuploadNotebookButton(container)
|
||||
];
|
||||
buttons.push(newNotebookButton);
|
||||
|
||||
if (container.gitHubOAuthService) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createManageGitHubAccountButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!container.isRunningOnNationalCloud()) {
|
||||
if (!container.isNotebookEnabled()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createEnableNotebooksButton(container));
|
||||
}
|
||||
|
||||
if (container.isPreferredApiMongoDB()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createOpenMongoTerminalButton(container));
|
||||
}
|
||||
|
||||
if (container.isPreferredApiCassandra()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createOpenCassandraTerminalButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (container.isNotebookEnabled()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createOpenTerminalButton(container));
|
||||
|
||||
buttons.push(CommandBarComponentButtonFactory.createNotebookWorkspaceResetButton(container));
|
||||
|
||||
if (container.isGalleryEnabled()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createGalleryButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should be replaced with the create arcadia spark pool button
|
||||
// if (!container.isSparkEnabled() && container.isSparkEnabledForAccount()) {
|
||||
// const createSparkClusterButton = CommandBarComponentButtonFactory.createSparkClusterButton(container);
|
||||
// buttons.push(createSparkClusterButton);
|
||||
// }
|
||||
|
||||
// TODO: Should be replaced with the edit/manage/delete arcadia spark pool button
|
||||
// if (container.isSparkEnabled()) {
|
||||
// const manageSparkClusterButton = CommandBarComponentButtonFactory.createMonitorClusterButton(container);
|
||||
// manageSparkClusterButton.children = [
|
||||
// CommandBarComponentButtonFactory.createMonitorClusterButton(container),
|
||||
// CommandBarComponentButtonFactory.createEditClusterButton(container),
|
||||
// CommandBarComponentButtonFactory.createDeleteClusterButton(container),
|
||||
// CommandBarComponentButtonFactory.createLibraryManageButton(container),
|
||||
// CommandBarComponentButtonFactory.createClusterLibraryButton(container)
|
||||
// ];
|
||||
// buttons.push(manageSparkClusterButton);
|
||||
// }
|
||||
|
||||
if (!container.isDatabaseNodeOrNoneSelected()) {
|
||||
if (container.isNotebookEnabled()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createDivider());
|
||||
}
|
||||
|
||||
const isSqlQuerySupported = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
if (isSqlQuerySupported) {
|
||||
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
|
||||
buttons.push(newSqlQueryBtn);
|
||||
}
|
||||
|
||||
const isSupportedOpenQueryApi =
|
||||
container.isPreferredApiDocumentDB() || container.isPreferredApiMongoDB() || container.isPreferredApiGraph();
|
||||
const isSupportedOpenQueryFromDiskApi = container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
|
||||
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);
|
||||
openQueryBtn.children = [
|
||||
CommandBarComponentButtonFactory.createOpenQueryButton(container),
|
||||
CommandBarComponentButtonFactory.createOpenQueryFromDiskButton(container)
|
||||
];
|
||||
buttons.push(openQueryBtn);
|
||||
} else if (isSupportedOpenQueryFromDiskApi && container.selectedNode() && container.findSelectedCollection()) {
|
||||
buttons.push(CommandBarComponentButtonFactory.createOpenQueryFromDiskButton(container));
|
||||
}
|
||||
|
||||
if (CommandBarComponentButtonFactory.areScriptsSupported(container)) {
|
||||
const label = "New Stored Procedure";
|
||||
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
|
||||
newStoredProcedureBtn.children = CommandBarComponentButtonFactory.createScriptCommandButtons(container);
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public static createContextCommandBarButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
|
||||
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
|
||||
const label = "New Shell";
|
||||
const newMongoShellBtn: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && (<any>selectedCollection).onNewMongoShellClick();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public static createControlCommandBarButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
if (window.dataExplorerPlatform === PlatformType.Hosted) {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
if (!container.isPreferredApiCassandra()) {
|
||||
const label = "Settings";
|
||||
const settingsPaneButton: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: SettingsIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.settingsPane.open(),
|
||||
commandButtonLabel: null,
|
||||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
hasPopup: true,
|
||||
disabled: false
|
||||
};
|
||||
buttons.push(settingsPaneButton);
|
||||
}
|
||||
|
||||
if (container.isHostedDataExplorerEnabled()) {
|
||||
const label = "Open Full Screen";
|
||||
const fullScreenButton: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: OpenInTabIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.generateSharedAccessData(),
|
||||
commandButtonLabel: null,
|
||||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
hasPopup: false,
|
||||
disabled: !container.isHostedDataExplorerEnabled(),
|
||||
className: "OpenFullScreen"
|
||||
};
|
||||
buttons.push(fullScreenButton);
|
||||
}
|
||||
|
||||
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: FeedbackIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.provideFeedbackEmail(),
|
||||
commandButtonLabel: null,
|
||||
ariaLabel: label,
|
||||
tooltipText: label,
|
||||
hasPopup: false,
|
||||
disabled: false
|
||||
};
|
||||
buttons.push(feedbackButtonOptions);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public static createDivider(): ViewModels.NavbarButtonConfig {
|
||||
const label = `divider${CommandBarComponentButtonFactory.counter++}`;
|
||||
return {
|
||||
isDivider: true,
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
iconSrc: null,
|
||||
iconAlt: null,
|
||||
onCommandClick: null,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static areScriptsSupported(container: ViewModels.Explorer): boolean {
|
||||
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
|
||||
}
|
||||
|
||||
private static createNewCollectionGroup(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = container.addCollectionText();
|
||||
return {
|
||||
iconSrc: AddCollectionIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onNewCollectionClicked(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
id: "createNewContainerCommandButton"
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenSynapseLinkDialogButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
if (config.platform === Platform.Emulator) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
container.databaseAccount &&
|
||||
container.databaseAccount() &&
|
||||
container.databaseAccount().properties &&
|
||||
container.databaseAccount().properties.enableAnalyticalStorage
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capabilities =
|
||||
(container.databaseAccount &&
|
||||
container.databaseAccount() &&
|
||||
container.databaseAccount().properties &&
|
||||
container.databaseAccount().properties.capabilities) ||
|
||||
[];
|
||||
if (capabilities.some(capability => capability.name === Constants.CapabilityNames.EnableStorageAnalytics)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = "Enable Azure Synapse Link (Preview)";
|
||||
return {
|
||||
iconSrc: SynapseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: container.isSynapseLinkUpdating(),
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createNewDatabase(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = container.addDatabaseText();
|
||||
return {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
container.addDatabasePane.open();
|
||||
document.getElementById("linkAddDatabase").focus();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true
|
||||
};
|
||||
}
|
||||
|
||||
private static createNewSQLQueryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
|
||||
const label = "New SQL Query";
|
||||
return {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
} else if (container.isPreferredApiMongoDB()) {
|
||||
const label = "New Query";
|
||||
return {
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && (<any>selectedCollection).onNewMongoQueryClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static createScriptCommandButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
|
||||
const shouldEnableScriptsCommands: boolean =
|
||||
!container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container);
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Stored Procedure";
|
||||
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New UDF";
|
||||
const newUserDefinedFunctionBtn: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: AddUdfIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
buttons.push(newUserDefinedFunctionBtn);
|
||||
}
|
||||
|
||||
if (shouldEnableScriptsCommands) {
|
||||
const label = "New Trigger";
|
||||
const newTriggerBtn: ViewModels.NavbarButtonConfig = {
|
||||
iconSrc: AddTriggerIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, null);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
buttons.push(newTriggerBtn);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static createScaleAndSettingsButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
let isShared = false;
|
||||
if (container.isDatabaseNodeSelected()) {
|
||||
isShared = container.findSelectedDatabase().isDatabaseShared();
|
||||
} else if (container.isNodeKindSelected("Collection")) {
|
||||
const database: ViewModels.Database = container.findSelectedCollection().getDatabase();
|
||||
isShared = database && database.isDatabaseShared();
|
||||
}
|
||||
|
||||
const label = isShared ? "Settings" : "Scale & Settings";
|
||||
|
||||
return {
|
||||
iconSrc: ScaleIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && (<any>selectedCollection).onSettingsClick();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
}
|
||||
|
||||
private static createNewNotebookButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onNewNotebookClicked(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createuploadNotebookButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Upload to Notebook Server";
|
||||
return {
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onUploadToNotebookServerClicked(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenQueryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Query";
|
||||
return {
|
||||
iconSrc: BrowseQueriesIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.browseQueriesPane.open(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenQueryFromDiskButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Query From Disk";
|
||||
return {
|
||||
iconSrc: OpenQueryFromDiskIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.loadQueryPane.open(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
|
||||
private static createEnableNotebooksButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
if (config.platform === Platform.Emulator) {
|
||||
return null;
|
||||
}
|
||||
const label = "Enable Notebooks (Preview)";
|
||||
const tooltip =
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const description =
|
||||
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.setupNotebooksPane.openWithTitleAndDescription(label, description),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !container.isNotebooksEnabledForAccount(),
|
||||
ariaLabel: label,
|
||||
tooltipText: container.isNotebooksEnabledForAccount() ? "" : tooltip
|
||||
};
|
||||
}
|
||||
|
||||
private static createSparkClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Enable Spark";
|
||||
return {
|
||||
iconSrc: ApacheSparkIcon,
|
||||
iconAlt: "Enable spark icon",
|
||||
onCommandClick: () => container.setupSparkClusterPane.open(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createEditClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Edit Cluster";
|
||||
return {
|
||||
iconSrc: EditIcon,
|
||||
iconAlt: "Edit cluster icon",
|
||||
onCommandClick: () => container.manageSparkClusterPane.open(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createDeleteClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Delete Cluster";
|
||||
return {
|
||||
iconSrc: DeleteIcon,
|
||||
iconAlt: "Delete cluster icon",
|
||||
onCommandClick: () => container.deleteCluster(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createMonitorClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Monitor Cluster";
|
||||
return {
|
||||
iconSrc: ApacheSparkIcon,
|
||||
iconAlt: "Monitor cluster icon",
|
||||
onCommandClick: () => container.openSparkMasterTab(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
iconSrc: CosmosTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createGalleryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "View Gallery";
|
||||
return {
|
||||
iconSrc: GalleryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openGallery(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenMongoTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (container.isNotebookEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: disableButton,
|
||||
ariaLabel: label,
|
||||
tooltipText: !disableButton ? "" : tooltip
|
||||
};
|
||||
}
|
||||
|
||||
private static createOpenCassandraTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Open Cassandra Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton = !container.isNotebooksEnabledForAccount() && !container.isNotebookEnabled();
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (container.isNotebookEnabled()) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
container.setupNotebooksPane.openWithTitleAndDescription(title, description);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: disableButton,
|
||||
ariaLabel: label,
|
||||
tooltipText: !disableButton ? "" : tooltip
|
||||
};
|
||||
}
|
||||
|
||||
private static createNotebookWorkspaceResetButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Reset Workspace";
|
||||
return {
|
||||
iconSrc: ResetWorkspaceIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.resetNotebookWorkspace(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createManageGitHubAccountButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
let connectedToGitHub: boolean = container.gitHubOAuthService.isLoggedIn();
|
||||
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (!connectedToGitHub) {
|
||||
TelemetryProcessor.trace(Action.NotebooksGitHubConnect, ActionModifiers.Mark, {
|
||||
databaseAccountName: container.databaseAccount() && container.databaseAccount().name,
|
||||
defaultExperience: container.defaultExperience && container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
}
|
||||
container.gitHubReposPane.open();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createLibraryManageButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Manage Libraries";
|
||||
return {
|
||||
iconSrc: LibraryManageIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.libraryManagePane.open(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createClusterLibraryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
|
||||
const label = "Manage Cluster Libraries";
|
||||
return {
|
||||
iconSrc: LibraryManageIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.clusterLibraryPane.open(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static createStaticCommandBarButtonsForResourceToken(
|
||||
container: ViewModels.Explorer
|
||||
): ViewModels.NavbarButtonConfig[] {
|
||||
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
|
||||
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);
|
||||
|
||||
newSqlQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
|
||||
newSqlQueryBtn.onCommandClick = () => {
|
||||
const resourceTokenCollection: ViewModels.CollectionBase = container.resourceTokenCollection();
|
||||
resourceTokenCollection && resourceTokenCollection.onNewQueryClick(resourceTokenCollection, undefined);
|
||||
};
|
||||
|
||||
openQueryBtn.disabled = !container.isResourceTokenCollectionNodeSelected();
|
||||
if (!openQueryBtn.disabled) {
|
||||
openQueryBtn.children = [
|
||||
CommandBarComponentButtonFactory.createOpenQueryButton(container),
|
||||
CommandBarComponentButtonFactory.createOpenQueryFromDiskButton(container)
|
||||
];
|
||||
}
|
||||
|
||||
return [newSqlQueryBtn, openQueryBtn];
|
||||
}
|
||||
}
|
||||
82
src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx
Normal file
82
src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
|
||||
describe("CommandBarUtil tests", () => {
|
||||
const createButton = (): ViewModels.NavbarButtonConfig => {
|
||||
return {
|
||||
iconSrc: "icon",
|
||||
iconAlt: "label",
|
||||
onCommandClick: (e: React.SyntheticEvent): void => {},
|
||||
commandButtonLabel: "label",
|
||||
ariaLabel: "ariaLabel",
|
||||
hasPopup: true,
|
||||
disabled: true,
|
||||
tooltipText: "tooltipText",
|
||||
children: [],
|
||||
className: "className"
|
||||
};
|
||||
};
|
||||
|
||||
it("should convert simple NavbarButtonConfig button", () => {
|
||||
const btn = createButton();
|
||||
const backgroundColor = "backgroundColor";
|
||||
|
||||
const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
|
||||
expect(converteds.length).toBe(1);
|
||||
const converted = converteds[0];
|
||||
expect(!converted.split);
|
||||
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
|
||||
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
|
||||
expect(converted.onClick).toEqual(btn.onCommandClick);
|
||||
expect(converted.text).toEqual(btn.commandButtonLabel);
|
||||
expect(converted.ariaLabel).toEqual(btn.ariaLabel);
|
||||
expect(converted.disabled).toEqual(btn.disabled);
|
||||
expect(converted.className).toEqual(btn.className);
|
||||
});
|
||||
|
||||
it("should convert NavbarButtonConfig to split button", () => {
|
||||
const btn = createButton();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const child = createButton();
|
||||
child.commandButtonLabel = `child${i}`;
|
||||
btn.children.push(child);
|
||||
}
|
||||
|
||||
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
|
||||
expect(converteds.length).toBe(1);
|
||||
const converted = converteds[0];
|
||||
expect(converted.split);
|
||||
expect(converted.subMenuProps.items.length).toBe(btn.children.length);
|
||||
for (let i = 0; i < converted.subMenuProps.items.length; i++) {
|
||||
expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel);
|
||||
}
|
||||
});
|
||||
|
||||
it("should create buttons with unique keys", () => {
|
||||
const btns: ViewModels.NavbarButtonConfig[] = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
btns.push(createButton());
|
||||
}
|
||||
|
||||
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
|
||||
const keys = converteds.map((btn: ICommandBarItemProps) => btn.key);
|
||||
const uniqueKeys = converteds
|
||||
.map((btn: ICommandBarItemProps) => btn.key)
|
||||
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
|
||||
expect(uniqueKeys.length).toBe(btns.length);
|
||||
});
|
||||
|
||||
it("should create icon buttons with tooltips", () => {
|
||||
const btn = createButton();
|
||||
const backgroundColor = "backgroundColor";
|
||||
|
||||
btn.commandButtonLabel = null;
|
||||
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||
expect(converted.text).toEqual(btn.tooltipText);
|
||||
|
||||
converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
|
||||
delete btn.commandButtonLabel;
|
||||
expect(converted.text).toEqual(btn.tooltipText);
|
||||
});
|
||||
});
|
||||
183
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
Normal file
183
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Observable } from "knockout";
|
||||
import { IconType } from "office-ui-fabric-react/lib/Icon";
|
||||
import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities";
|
||||
import { KeyCodes, StyleConstants } from "../../../Common/Constants";
|
||||
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { Dropdown, DropdownMenuItemType, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||
|
||||
/**
|
||||
* Utilities for CommandBar
|
||||
*/
|
||||
export class CommandBarUtil {
|
||||
/**
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
* @param btns
|
||||
*/
|
||||
public static convertButton(btns: ViewModels.NavbarButtonConfig[], backgroundColor: string): ICommandBarItemProps[] {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
return btns
|
||||
.filter(btn => btn)
|
||||
.map(
|
||||
(btn: ViewModels.NavbarButtonConfig, index: number): ICommandBarItemProps => {
|
||||
if (btn.isDivider) {
|
||||
return CommandBarUtil.createDivider(btn.commandButtonLabel);
|
||||
}
|
||||
|
||||
const isSplit = !!btn.children && btn.children.length > 0;
|
||||
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth // 16
|
||||
},
|
||||
imageProps: { src: btn.iconSrc, alt: btn.iconAlt }
|
||||
},
|
||||
onClick: btn.onCommandClick,
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
text: btn.commandButtonLabel || btn.tooltipText,
|
||||
"data-test": btn.commandButtonLabel || btn.tooltipText,
|
||||
title: btn.tooltipText,
|
||||
name: "menuitem",
|
||||
disabled: btn.disabled,
|
||||
ariaLabel: btn.ariaLabel,
|
||||
buttonStyles: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
height: buttonHeightPx,
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
minWidth: 24,
|
||||
marginLeft: isSplit ? 0 : 5,
|
||||
marginRight: isSplit ? 0 : 5
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: backgroundColor,
|
||||
pointerEvents: "auto"
|
||||
},
|
||||
splitButtonMenuButton: {
|
||||
backgroundColor: backgroundColor,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
},
|
||||
width: 16
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
backgroundColor: StyleConstants.AccentExtra,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
}
|
||||
},
|
||||
splitButtonDivider: {
|
||||
display: "none"
|
||||
},
|
||||
icon: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0
|
||||
},
|
||||
splitButtonContainer: {
|
||||
marginLeft: 5,
|
||||
marginRight: 5
|
||||
}
|
||||
},
|
||||
className: btn.className,
|
||||
id: btn.id
|
||||
};
|
||||
|
||||
if (isSplit) {
|
||||
// It's a split button
|
||||
result.split = true;
|
||||
|
||||
result.subMenuProps = {
|
||||
items: CommandBarUtil.convertButton(btn.children, backgroundColor),
|
||||
styles: {
|
||||
list: {
|
||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result.menuIconProps = {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isDropdown) {
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
root: { margin: 5 },
|
||||
dropdown: { width: btn.dropdownWidth },
|
||||
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
||||
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
||||
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
||||
};
|
||||
|
||||
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
||||
return (
|
||||
<Dropdown
|
||||
placeholder={btn.dropdownPlaceholder}
|
||||
defaultSelectedKey={btn.dropdownSelectedKey}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number): void =>
|
||||
btn.children[index].onCommandClick(event)
|
||||
}
|
||||
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
||||
key: child.dropdownItemKey,
|
||||
text: child.commandButtonLabel
|
||||
}))}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
||||
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static createDivider(key: string): ICommandBarItemProps {
|
||||
return {
|
||||
onRender: () => (
|
||||
<div className="dividerContainer">
|
||||
<span />
|
||||
</div>
|
||||
),
|
||||
iconOnly: true,
|
||||
disabled: true,
|
||||
key: key
|
||||
};
|
||||
}
|
||||
|
||||
public static createMemoryTracker(key: string, memoryUsageInfo: Observable<MemoryUsageInfo>): ICommandBarItemProps {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
||||
};
|
||||
}
|
||||
}
|
||||
24
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less
Normal file
24
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less
Normal file
@@ -0,0 +1,24 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.memoryTrackerContainer {
|
||||
cursor: default;
|
||||
align-items: center;
|
||||
margin: 0 9px;
|
||||
border: 1px;
|
||||
|
||||
> span {
|
||||
padding-right: 12px;
|
||||
font-size: 13px;
|
||||
font-family: @DataExplorerFont;
|
||||
color: @DefaultFontColor;
|
||||
}
|
||||
|
||||
> .lowMemory {
|
||||
.ms-ProgressIndicator-progressBar {
|
||||
background-color: @SelectionHigh;
|
||||
}
|
||||
.ms-ProgressIndicator-itemDescription {
|
||||
color: @SelectionHigh;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
Normal file
50
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from "react";
|
||||
import { Observable, Subscription } from "knockout";
|
||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||
import { ProgressIndicator } from "office-ui-fabric-react/lib/ProgressIndicator";
|
||||
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner";
|
||||
import { Stack } from "office-ui-fabric-react/lib/Stack";
|
||||
|
||||
interface MemoryTrackerProps {
|
||||
memoryUsageInfo: Observable<MemoryUsageInfo>;
|
||||
}
|
||||
|
||||
export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps> {
|
||||
private memoryUsageInfoSubscription: Subscription;
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.memoryUsageInfoSubscription = this.props.memoryUsageInfo.subscribe(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
this.memoryUsageInfoSubscription && this.memoryUsageInfoSubscription.dispose();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const memoryUsageInfo: MemoryUsageInfo = this.props.memoryUsageInfo();
|
||||
if (!memoryUsageInfo) {
|
||||
return (
|
||||
<Stack className="memoryTrackerContainer" horizontal>
|
||||
<span>Memory</span>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const totalGB = memoryUsageInfo.totalKB / 1048576;
|
||||
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
|
||||
|
||||
return (
|
||||
<Stack className="memoryTrackerContainer" horizontal>
|
||||
<span>Memory</span>
|
||||
<ProgressIndicator
|
||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||
percentComplete={usedGB / totalGB}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
43
src/Explorer/Menus/ContextMenu.ts
Normal file
43
src/Explorer/Menus/ContextMenu.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { CommandButtonOptions } from "./../Controls/CommandButton/CommandButton";
|
||||
|
||||
export default class ContextMenu implements ViewModels.ContextMenu {
|
||||
public container: ViewModels.Explorer;
|
||||
public visible: ko.Observable<boolean>;
|
||||
public elementId: string;
|
||||
public options: ko.ObservableArray<CommandButtonOptions>;
|
||||
public tabIndex: ko.Observable<number>;
|
||||
|
||||
constructor(container: ViewModels.Explorer, rid: string) {
|
||||
this.container = container;
|
||||
this.visible = ko.observable<boolean>(false);
|
||||
this.elementId = `contextMenu${rid}`;
|
||||
this.options = ko.observableArray<CommandButtonOptions>([]);
|
||||
this.tabIndex = ko.observable<number>(0);
|
||||
}
|
||||
|
||||
public show(source: ViewModels.TreeNode, event: MouseEvent) {
|
||||
if (source && source.contextMenu && source.contextMenu.visible && source.contextMenu.visible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.selectedNode(source);
|
||||
const elementId = source.contextMenu.elementId;
|
||||
const htmlElement = document.getElementById(elementId);
|
||||
htmlElement.style.left = `${event.clientX}px`;
|
||||
htmlElement.style.top = `${event.clientY}px`;
|
||||
|
||||
!!source.contextMenu && source.contextMenu.visible(true);
|
||||
source.contextMenu.tabIndex(0);
|
||||
htmlElement.focus();
|
||||
}
|
||||
|
||||
public hide(source: ViewModels.TreeNode, event: MouseEvent) {
|
||||
if (!source || !source.contextMenu || !source.contextMenu.visible || !source.contextMenu.visible()) {
|
||||
return;
|
||||
}
|
||||
source.contextMenu.tabIndex(-1);
|
||||
source.contextMenu.visible(false);
|
||||
}
|
||||
}
|
||||
31
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
Normal file
31
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* React component for control bar
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandButtonComponent } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
export interface ControlBarComponentProps {
|
||||
buttons: ViewModels.NavbarButtonConfig[];
|
||||
}
|
||||
|
||||
export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
|
||||
private static renderButtons(commandButtonOptions: ViewModels.NavbarButtonConfig[]): JSX.Element[] {
|
||||
return commandButtonOptions.map(
|
||||
(btn: ViewModels.NavbarButtonConfig, index: number): JSX.Element => {
|
||||
// Remove label
|
||||
btn.commandButtonLabel = null;
|
||||
return CommandButtonComponent.renderButton(btn, `${index}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.buttons || this.props.buttons.length < 1) {
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
||||
return <React.Fragment>{ControlBarComponent.renderButtons(this.props.buttons)}</React.Fragment>;
|
||||
}
|
||||
}
|
||||
28
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
Normal file
28
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* This adapter is responsible to render the React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { ControlBarComponent } from "./ControlBarComponent";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
|
||||
export class ControlBarComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private buttons: ko.ObservableArray<ViewModels.NavbarButtonConfig>) {
|
||||
this.buttons.subscribe(() => this.forceRender());
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <ControlBarComponent buttons={this.buttons()} />;
|
||||
}
|
||||
|
||||
public forceRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
93
src/Explorer/Menus/NavBar/MeControlComponent.less
Normal file
93
src/Explorer/Menus/NavBar/MeControlComponent.less
Normal file
@@ -0,0 +1,93 @@
|
||||
.meControl {
|
||||
.mecontrolHeaderButton {
|
||||
height : auto;
|
||||
border : 0;
|
||||
margin : 0;
|
||||
padding : 0 10px;
|
||||
background-color: black;
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
background-color: #393939
|
||||
}
|
||||
|
||||
.mecontrolHeaderPersona {
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
|
||||
.ms-Persona-details {
|
||||
order : -1;
|
||||
text-align : right;
|
||||
padding-left : 0;
|
||||
padding-right: 10;
|
||||
|
||||
.ms-Persona-primaryText {
|
||||
line-height: 18px;
|
||||
color : white;
|
||||
}
|
||||
|
||||
.ms-Persona-secondaryText {
|
||||
color : white;
|
||||
font-size : 10px;
|
||||
font-family: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
:hover .ms-Persona-details .ms-Persona-primaryText {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mecontrolSigninButton {
|
||||
height : 40px;
|
||||
border : 0;
|
||||
background-color: black;
|
||||
color : white;
|
||||
}
|
||||
}
|
||||
|
||||
.mecontrolContextualMenu {
|
||||
.mecontrolContextualMenuPersona {
|
||||
margin : 16px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.ms-Persona-details {
|
||||
padding: 0;
|
||||
margin : -32px 0 0 16;
|
||||
|
||||
.ms-Persona-primaryText {
|
||||
color : black;
|
||||
font-size : 18px;
|
||||
height : 20px;
|
||||
font-family: "Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.ms-Persona-secondaryText {
|
||||
color : black;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switchDirectoryLink,
|
||||
.signOutLink {
|
||||
color : #0078d6;
|
||||
margin-left: 104px;
|
||||
font-size : 14px;
|
||||
cursor : pointer;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.switchDirectoryLink {
|
||||
margin-top: -32px;
|
||||
}
|
||||
|
||||
.signOutLink {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
108
src/Explorer/Menus/NavBar/MeControlComponent.test.tsx
Normal file
108
src/Explorer/Menus/NavBar/MeControlComponent.test.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { MeControlComponent, MeControlComponentProps } from "./MeControlComponent";
|
||||
|
||||
const createNotSignedInProps = (): MeControlComponentProps => {
|
||||
return {
|
||||
isUserSignedIn: false,
|
||||
user: null,
|
||||
onSignInClick: jest.fn(),
|
||||
onSignOutClick: jest.fn(),
|
||||
onSwitchDirectoryClick: jest.fn()
|
||||
};
|
||||
};
|
||||
|
||||
const createSignedInProps = (): MeControlComponentProps => {
|
||||
return {
|
||||
isUserSignedIn: true,
|
||||
user: {
|
||||
name: "Test User",
|
||||
email: "testuser@contoso.com",
|
||||
tenantName: "Contoso",
|
||||
imageUrl: "../../../../images/dotnet.png"
|
||||
},
|
||||
onSignInClick: jest.fn(),
|
||||
onSignOutClick: jest.fn(),
|
||||
onSwitchDirectoryClick: jest.fn()
|
||||
};
|
||||
};
|
||||
|
||||
describe("test render", () => {
|
||||
it("renders not signed in", () => {
|
||||
const props = createNotSignedInProps();
|
||||
|
||||
const wrapper = shallow(<MeControlComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders signed in with full info", () => {
|
||||
const props = createSignedInProps();
|
||||
|
||||
const wrapper = shallow(<MeControlComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("change not signed in to signed in", () => {
|
||||
const notSignInProps = createNotSignedInProps();
|
||||
|
||||
const wrapper = mount(<MeControlComponent {...notSignInProps} />);
|
||||
expect(wrapper.exists(".mecontrolSigninButton")).toBe(true);
|
||||
expect(wrapper.exists(".mecontrolHeaderButton")).toBe(false);
|
||||
|
||||
const signInProps = createSignedInProps();
|
||||
|
||||
wrapper.setProps(signInProps);
|
||||
expect(wrapper.exists(".mecontrolSigninButton")).toBe(false);
|
||||
expect(wrapper.exists(".mecontrolHeaderButton")).toBe(true);
|
||||
|
||||
wrapper.unmount;
|
||||
});
|
||||
|
||||
it("render contextual menu", () => {
|
||||
const signInProps = createSignedInProps();
|
||||
const wrapper = mount(<MeControlComponent {...signInProps} />);
|
||||
|
||||
wrapper.find("button.mecontrolHeaderButton").simulate("click");
|
||||
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true);
|
||||
|
||||
wrapper.find("button.mecontrolHeaderButton").simulate("click");
|
||||
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(false);
|
||||
|
||||
wrapper.unmount;
|
||||
});
|
||||
});
|
||||
|
||||
describe("test function got called", () => {
|
||||
it("sign in click", () => {
|
||||
const notSignInProps = createNotSignedInProps();
|
||||
const wrapper = mount(<MeControlComponent {...notSignInProps} />);
|
||||
|
||||
wrapper.find("button.mecontrolSigninButton").simulate("click");
|
||||
expect(notSignInProps.onSignInClick).toBeCalled();
|
||||
expect(notSignInProps.onSignInClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sign out click", () => {
|
||||
const signInProps = createSignedInProps();
|
||||
const wrapper = mount(<MeControlComponent {...signInProps} />);
|
||||
|
||||
wrapper.find("button.mecontrolHeaderButton").simulate("click");
|
||||
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true);
|
||||
|
||||
wrapper.find("div.signOutLink").simulate("click");
|
||||
expect(signInProps.onSignOutClick).toBeCalled();
|
||||
expect(signInProps.onSignOutClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("switch directory", () => {
|
||||
const signInProps = createSignedInProps();
|
||||
const wrapper = mount(<MeControlComponent {...signInProps} />);
|
||||
|
||||
wrapper.find("button.mecontrolHeaderButton").simulate("click");
|
||||
expect(wrapper.exists(".mecontrolContextualMenu")).toBe(true);
|
||||
|
||||
wrapper.find("div.switchDirectoryLink").simulate("click");
|
||||
expect(signInProps.onSwitchDirectoryClick).toBeCalled();
|
||||
expect(signInProps.onSwitchDirectoryClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
167
src/Explorer/Menus/NavBar/MeControlComponent.tsx
Normal file
167
src/Explorer/Menus/NavBar/MeControlComponent.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import * as React from "react";
|
||||
import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { FocusZone } from "office-ui-fabric-react/lib/FocusZone";
|
||||
import { IPersonaSharedProps, Persona, PersonaInitialsColor, PersonaSize } from "office-ui-fabric-react/lib/Persona";
|
||||
|
||||
export interface MeControlComponentProps {
|
||||
/**
|
||||
* Wheather user is signed in or not
|
||||
*/
|
||||
isUserSignedIn: boolean;
|
||||
/**
|
||||
* User info
|
||||
*/
|
||||
user: MeControlUser;
|
||||
/**
|
||||
* Click handler for sign in click
|
||||
*/
|
||||
onSignInClick: (e: React.MouseEvent<BaseButton>) => void;
|
||||
/**
|
||||
* Click handler for sign out click
|
||||
*/
|
||||
onSignOutClick: (e: React.SyntheticEvent) => void;
|
||||
/**
|
||||
* Click handler for switch directory click
|
||||
*/
|
||||
onSwitchDirectoryClick: (e: React.SyntheticEvent) => void;
|
||||
}
|
||||
|
||||
export interface MeControlUser {
|
||||
/**
|
||||
* Display name for user
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Display email for user
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* Display tenant for user
|
||||
*/
|
||||
tenantName: string;
|
||||
/**
|
||||
* image source for the profic photo
|
||||
*/
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
export class MeControlComponent extends React.Component<MeControlComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return this.props.isUserSignedIn ? this._renderProfileComponent() : this._renderSignInComponent();
|
||||
}
|
||||
|
||||
private _renderProfileComponent(): JSX.Element {
|
||||
const { user } = this.props;
|
||||
|
||||
const menuProps: IContextualMenuProps = {
|
||||
className: "mecontrolContextualMenu",
|
||||
isBeakVisible: false,
|
||||
directionalHintFixed: true,
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
calloutProps: {
|
||||
minPagePadding: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: "Persona",
|
||||
onRender: this._renderPersonaComponent
|
||||
},
|
||||
{
|
||||
key: "SwitchDirectory",
|
||||
onRender: this._renderSwitchDirectory
|
||||
},
|
||||
{
|
||||
key: "SignOut",
|
||||
onRender: this._renderSignOut
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const personaProps: IPersonaSharedProps = {
|
||||
imageUrl: user.imageUrl,
|
||||
text: user.email,
|
||||
secondaryText: user.tenantName,
|
||||
showSecondaryText: true,
|
||||
showInitialsUntilImageLoads: true,
|
||||
initialsColor: PersonaInitialsColor.teal,
|
||||
size: PersonaSize.size28,
|
||||
className: "mecontrolHeaderPersona"
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
id: "mecontrolHeader",
|
||||
className: "mecontrolHeaderButton",
|
||||
menuProps: menuProps,
|
||||
onRenderMenuIcon: () => <span />,
|
||||
styles: {
|
||||
rootHovered: { backgroundColor: "#393939" },
|
||||
rootFocused: { backgroundColor: "#393939" },
|
||||
rootPressed: { backgroundColor: "#393939" },
|
||||
rootExpanded: { backgroundColor: "#393939" }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FocusZone>
|
||||
<DefaultButton {...buttonProps}>
|
||||
<Persona {...personaProps} />
|
||||
</DefaultButton>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPersonaComponent = (): JSX.Element => {
|
||||
const { user } = this.props;
|
||||
const personaProps: IPersonaSharedProps = {
|
||||
imageUrl: user.imageUrl,
|
||||
text: user.name,
|
||||
secondaryText: user.email,
|
||||
showSecondaryText: true,
|
||||
showInitialsUntilImageLoads: true,
|
||||
initialsColor: PersonaInitialsColor.teal,
|
||||
size: PersonaSize.size72,
|
||||
className: "mecontrolContextualMenuPersona"
|
||||
};
|
||||
|
||||
return <Persona {...personaProps} />;
|
||||
};
|
||||
|
||||
private _renderSwitchDirectory = (): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className="switchDirectoryLink"
|
||||
onClick={(e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) =>
|
||||
this.props.onSwitchDirectoryClick(e)
|
||||
}
|
||||
>
|
||||
Switch Directory
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private _renderSignOut = (): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className="signOutLink"
|
||||
onClick={(e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => this.props.onSignOutClick(e)}
|
||||
>
|
||||
Sign out
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
private _renderSignInComponent = (): JSX.Element => {
|
||||
const buttonProps: IButtonProps = {
|
||||
className: "mecontrolSigninButton",
|
||||
text: "Sign In",
|
||||
onClick: (e: React.MouseEvent<BaseButton>) => this.props.onSignInClick(e),
|
||||
styles: {
|
||||
rootHovered: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootFocused: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootPressed: { backgroundColor: "#393939", color: "#fff" }
|
||||
}
|
||||
};
|
||||
return <DefaultButton {...buttonProps} />;
|
||||
};
|
||||
}
|
||||
16
src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx
Normal file
16
src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* This adapter is responsible to render the React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { MeControlComponent, MeControlComponentProps } from "./MeControlComponent";
|
||||
|
||||
export class MeControlComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<MeControlComponentProps>;
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <MeControlComponent {...this.parameters()} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`test render renders not signed in 1`] = `
|
||||
<CustomizedDefaultButton
|
||||
className="mecontrolSigninButton"
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"rootFocused": Object {
|
||||
"backgroundColor": "#393939",
|
||||
"color": "#fff",
|
||||
},
|
||||
"rootHovered": Object {
|
||||
"backgroundColor": "#393939",
|
||||
"color": "#fff",
|
||||
},
|
||||
"rootPressed": Object {
|
||||
"backgroundColor": "#393939",
|
||||
"color": "#fff",
|
||||
},
|
||||
}
|
||||
}
|
||||
text="Sign In"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test render renders signed in with full info 1`] = `
|
||||
<FocusZone
|
||||
direction={2}
|
||||
isCircularNavigation={false}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
className="mecontrolHeaderButton"
|
||||
id="mecontrolHeader"
|
||||
menuProps={
|
||||
Object {
|
||||
"calloutProps": Object {
|
||||
"minPagePadding": 0,
|
||||
},
|
||||
"className": "mecontrolContextualMenu",
|
||||
"directionalHint": 6,
|
||||
"directionalHintFixed": true,
|
||||
"isBeakVisible": false,
|
||||
"items": Array [
|
||||
Object {
|
||||
"key": "Persona",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"key": "SwitchDirectory",
|
||||
"onRender": [Function],
|
||||
},
|
||||
Object {
|
||||
"key": "SignOut",
|
||||
"onRender": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
onRenderMenuIcon={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"rootExpanded": Object {
|
||||
"backgroundColor": "#393939",
|
||||
},
|
||||
"rootFocused": Object {
|
||||
"backgroundColor": "#393939",
|
||||
},
|
||||
"rootHovered": Object {
|
||||
"backgroundColor": "#393939",
|
||||
},
|
||||
"rootPressed": Object {
|
||||
"backgroundColor": "#393939",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledPersonaBase
|
||||
className="mecontrolHeaderPersona"
|
||||
imageUrl="../../../../images/dotnet.png"
|
||||
initialsColor={3}
|
||||
secondaryText="Contoso"
|
||||
showInitialsUntilImageLoads={true}
|
||||
showSecondaryText={true}
|
||||
size={7}
|
||||
text="testuser@contoso.com"
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</FocusZone>
|
||||
`;
|
||||
160
src/Explorer/Menus/NotificationConsole/NotificationConsole.less
Normal file
160
src/Explorer/Menus/NotificationConsole/NotificationConsole.less
Normal file
@@ -0,0 +1,160 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
@ConsoleHeaderHeight: 32px;
|
||||
@ConsoleContentsPaneHeight: 220px;
|
||||
@ConsoleStatusMaxWidth: 672px;
|
||||
|
||||
@ConsoleIconSize: 12px;
|
||||
@ExpandCollapseIconSize: 20px;
|
||||
|
||||
.notificationConsoleContainer {
|
||||
width: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
img {
|
||||
width: @ConsoleIconSize;
|
||||
height: @ConsoleIconSize;
|
||||
}
|
||||
|
||||
.notificationConsoleHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
height: @ConsoleHeaderHeight;
|
||||
width: 100%;
|
||||
background-color: @NotificationLow;
|
||||
border-top: @ButtonBorderWidth @BaseMedium solid;
|
||||
cursor: pointer;
|
||||
flex-shrink:0;
|
||||
|
||||
&:hover {
|
||||
background-color:@NotificationHigh;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color:@NotificationHigh;
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
.dataTypeIcons {
|
||||
cursor: pointer;
|
||||
margin: 0px @DefaultSpace 0px @MediumSpace;
|
||||
padding-left: @DefaultSpace;
|
||||
|
||||
.notificationConsoleHeaderIconWithData{
|
||||
&:not(:last-child) {
|
||||
padding-right: @LargeSpace;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-bottom: @SmallSpace;
|
||||
margin-right: @DefaultSpace;
|
||||
}
|
||||
|
||||
.numInProgress, .numErroredItems, .numInfoItems {
|
||||
padding-left: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.consoleSplitter {
|
||||
border-left: 1px solid @BaseMedium;
|
||||
margin-right: @LargeSpace;
|
||||
padding: 0px 0px 2px;
|
||||
}
|
||||
|
||||
.headerStatus {
|
||||
display: inline-flex;
|
||||
|
||||
.headerStatusEllipsis {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: @ConsoleStatusMaxWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expandCollapseButton {
|
||||
cursor: pointer;
|
||||
padding-right: 5px;
|
||||
|
||||
img {
|
||||
width: @ExpandCollapseIconSize;
|
||||
height: @ExpandCollapseIconSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationConsoleContents {
|
||||
width: 100%;
|
||||
height: @ConsoleContentsPaneHeight;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: @BaseLow;
|
||||
|
||||
.notificationConsoleControls {
|
||||
padding: @MediumSpace;
|
||||
margin-left:@DefaultSpace;
|
||||
|
||||
#consoleFilterLabel {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.consoleSplitter {
|
||||
border-left: 1px solid @BaseMedium;
|
||||
margin: @MediumSpace;
|
||||
}
|
||||
|
||||
.clearNotificationsButton {
|
||||
cursor: pointer;
|
||||
padding:@SmallSpace;
|
||||
border:@ButtonBorderWidth solid @BaseLow;
|
||||
|
||||
&:hover {
|
||||
background-color:@BaseMediumLow;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: @ButtonBorderWidth dashed @AccentMedium;
|
||||
background-color: @AccentMediumLow;
|
||||
}
|
||||
|
||||
img{
|
||||
margin-bottom:@SmallSpace;
|
||||
margin-right:2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationConsoleData {
|
||||
overflow-y: auto;
|
||||
overflow-x:hidden;
|
||||
margin-left:@LargeSpace;
|
||||
|
||||
.rowData {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: @SmallSpace;
|
||||
|
||||
img {
|
||||
margin-top:@SmallSpace;
|
||||
}
|
||||
|
||||
.date {
|
||||
margin: 0px @LargeSpace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message {
|
||||
flex-grow: 1;
|
||||
white-space:pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
NotificationConsoleComponentProps,
|
||||
ConsoleData,
|
||||
NotificationConsoleComponent,
|
||||
ConsoleDataType
|
||||
} from "./NotificationConsoleComponent";
|
||||
|
||||
describe("NotificationConsoleComponent", () => {
|
||||
const createBlankProps = (): NotificationConsoleComponentProps => {
|
||||
return {
|
||||
consoleData: [],
|
||||
isConsoleExpanded: true,
|
||||
onConsoleDataChange: (consoleData: ConsoleData[]) => {},
|
||||
onConsoleExpandedChange: (isExpanded: boolean) => {}
|
||||
};
|
||||
};
|
||||
|
||||
it("renders the console (expanded)", () => {
|
||||
const props = createBlankProps();
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.Info,
|
||||
date: "date",
|
||||
message: "message"
|
||||
});
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows proper progress count", () => {
|
||||
const count = 100;
|
||||
const props = createBlankProps();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.InProgress,
|
||||
date: "date",
|
||||
message: "message"
|
||||
});
|
||||
}
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual(count.toString());
|
||||
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
|
||||
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
|
||||
});
|
||||
|
||||
it("shows proper error count", () => {
|
||||
const count = 100;
|
||||
const props = createBlankProps();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.Error,
|
||||
date: "date",
|
||||
message: "message"
|
||||
});
|
||||
}
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
||||
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual(count.toString());
|
||||
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
|
||||
});
|
||||
|
||||
it("shows proper info count", () => {
|
||||
const count = 100;
|
||||
const props = createBlankProps();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.Info,
|
||||
date: "date",
|
||||
message: "message"
|
||||
});
|
||||
}
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
||||
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
|
||||
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual(count.toString());
|
||||
});
|
||||
|
||||
const testRenderNotification = (date: string, msg: string, type: ConsoleDataType, iconClassName: string) => {
|
||||
const props = createBlankProps();
|
||||
props.consoleData.push({
|
||||
date: date,
|
||||
message: msg,
|
||||
type: type
|
||||
});
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
|
||||
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(msg);
|
||||
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
|
||||
};
|
||||
|
||||
it("renders progress notifications", () => {
|
||||
testRenderNotification("date", "message", ConsoleDataType.InProgress, "loaderIcon");
|
||||
});
|
||||
|
||||
it("renders error notifications", () => {
|
||||
testRenderNotification("date", "message", ConsoleDataType.Error, "errorIcon");
|
||||
});
|
||||
|
||||
it("renders info notifications", () => {
|
||||
testRenderNotification("date", "message", ConsoleDataType.Info, "infoIcon");
|
||||
});
|
||||
|
||||
it("clears notifications", () => {
|
||||
const props = createBlankProps();
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.InProgress,
|
||||
date: "date",
|
||||
message: "message1"
|
||||
});
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.Error,
|
||||
date: "date",
|
||||
message: "message2"
|
||||
});
|
||||
props.consoleData.push({
|
||||
type: ConsoleDataType.Info,
|
||||
date: "date",
|
||||
message: "message3"
|
||||
});
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
wrapper.find(".clearNotificationsButton").simulate("click");
|
||||
|
||||
expect(!wrapper.exists(".notificationConsoleData"));
|
||||
});
|
||||
|
||||
it("collapses and hide content", () => {
|
||||
const props = createBlankProps();
|
||||
props.consoleData.push({
|
||||
date: "date",
|
||||
message: "message",
|
||||
type: ConsoleDataType.Info
|
||||
});
|
||||
props.isConsoleExpanded = true;
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||
wrapper.find(".notificationConsoleHeader").simulate("click");
|
||||
expect(!wrapper.exists(".notificationConsoleContent"));
|
||||
});
|
||||
|
||||
it("display latest data in header", () => {
|
||||
const latestData = "latest data";
|
||||
const props1 = createBlankProps();
|
||||
const props2 = createBlankProps();
|
||||
props2.consoleData.push({
|
||||
date: "date",
|
||||
message: latestData,
|
||||
type: ConsoleDataType.Info
|
||||
});
|
||||
props2.isConsoleExpanded = true;
|
||||
|
||||
const wrapper = shallow(<NotificationConsoleComponent {...props1} />);
|
||||
wrapper.setProps(props2);
|
||||
expect(wrapper.find(".headerStatusEllipsis").text()).toEqual(latestData);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* React component for control bar
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
|
||||
import LoadingIcon from "../../../../images/loading.svg";
|
||||
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
import InfoIcon from "../../../../images/info_color.svg";
|
||||
import ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
|
||||
import ClearIcon from "../../../../images/Clear.svg";
|
||||
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
|
||||
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
|
||||
/**
|
||||
* Log levels
|
||||
*/
|
||||
export enum ConsoleDataType {
|
||||
Info = 0,
|
||||
Error = 1,
|
||||
InProgress = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the data/content that will be recorded
|
||||
*/
|
||||
export interface ConsoleData {
|
||||
type: ConsoleDataType;
|
||||
date: string;
|
||||
message: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface NotificationConsoleComponentProps {
|
||||
isConsoleExpanded: boolean;
|
||||
onConsoleExpandedChange: (isExpanded: boolean) => void;
|
||||
consoleData: ConsoleData[];
|
||||
onConsoleDataChange: (consoleData: ConsoleData[]) => void;
|
||||
}
|
||||
|
||||
interface NotificationConsoleComponentState {
|
||||
headerStatus: string;
|
||||
selectedFilter: string;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
export class NotificationConsoleComponent extends React.Component<
|
||||
NotificationConsoleComponentProps,
|
||||
NotificationConsoleComponentState
|
||||
> {
|
||||
private static readonly transitionDurationMs = 200;
|
||||
private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
|
||||
private headerTimeoutId: number;
|
||||
private prevHeaderStatus: string;
|
||||
private consoleHeaderElement: HTMLElement;
|
||||
|
||||
constructor(props: NotificationConsoleComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
headerStatus: "",
|
||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0],
|
||||
isExpanded: props.isConsoleExpanded
|
||||
};
|
||||
this.prevHeaderStatus = null;
|
||||
}
|
||||
|
||||
public componentDidUpdate(
|
||||
prevProps: NotificationConsoleComponentProps,
|
||||
prevState: NotificationConsoleComponentState
|
||||
) {
|
||||
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props);
|
||||
|
||||
if (
|
||||
this.prevHeaderStatus !== currentHeaderStatus &&
|
||||
currentHeaderStatus !== null &&
|
||||
prevState.headerStatus !== currentHeaderStatus
|
||||
) {
|
||||
this.setHeaderStatus(currentHeaderStatus);
|
||||
}
|
||||
|
||||
// Call setHeaderStatus() only to clear HeaderStatus or update status to a different value.
|
||||
// Cache previous headerStatus externally. Otherwise, simply comparing with previous state/props will cause circular
|
||||
// updates: currentHeaderStatus -> "" -> currentHeaderStatus -> "" etc.
|
||||
this.prevHeaderStatus = currentHeaderStatus;
|
||||
|
||||
if (prevProps.isConsoleExpanded !== this.props.isConsoleExpanded) {
|
||||
// Sync state and props
|
||||
// TODO react anti-pattern: remove isExpanded from state which duplicates prop's isConsoleExpanded
|
||||
this.setState({ isExpanded: this.props.isConsoleExpanded });
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress)
|
||||
.length;
|
||||
const numErroredItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error)
|
||||
.length;
|
||||
const numInfoItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info)
|
||||
.length;
|
||||
return (
|
||||
<div className="notificationConsoleContainer">
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
ref={(element: HTMLElement) => (this.consoleHeaderElement = element)}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="statusBar">
|
||||
<span className="dataTypeIcons">
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={LoadingIcon} alt="in progress items" />
|
||||
<span className="numInProgress">{numInProgress}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={ErrorBlackIcon} alt="error items" />
|
||||
<span className="numErroredItems">{numErroredItems}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={infoBubbleIcon} alt="info items" />
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="consoleSplitter" />
|
||||
<span className="headerStatus">
|
||||
<span className="headerStatusEllipsis">{this.state.headerStatus}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="expandCollapseButton" role="button" tabIndex={0}>
|
||||
<img
|
||||
src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||
alt={this.state.isExpanded ? "collapse console" : "expand console"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
duration={NotificationConsoleComponent.transitionDurationMs}
|
||||
height={this.state.isExpanded ? "auto" : 0}
|
||||
onAnimationEnd={this.onConsoleWasExpanded}
|
||||
>
|
||||
<div className="notificationConsoleContents">
|
||||
<div className="notificationConsoleControls">
|
||||
<label id="consoleFilterLabel">Filter</label>
|
||||
<select
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
role="combobox"
|
||||
aria-label={this.state.selectedFilter}
|
||||
value={this.state.selectedFilter}
|
||||
onChange={this.onFilterSelected.bind(this)}
|
||||
>
|
||||
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
|
||||
<option value={value} key={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="consoleSplitter" />
|
||||
<span
|
||||
className="clearNotificationsButton"
|
||||
onClick={() => this.clearNotifications()}
|
||||
role="button"
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<img src={ClearIcon} alt="clear notifications image" />
|
||||
Clear Notifications
|
||||
</span>
|
||||
</div>
|
||||
<div className="notificationConsoleData">
|
||||
{this.renderAllFilteredConsoleData(this.getFilteredConsoleData())}
|
||||
</div>
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private expandCollapseConsole() {
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
}
|
||||
|
||||
private onExpandCollapseKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||
this.expandCollapseConsole();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onClearNotificationsKeyPress = (event: React.KeyboardEvent<HTMLSpanElement>): void => {
|
||||
if (event.keyCode === KeyCodes.Space || event.keyCode === KeyCodes.Enter) {
|
||||
this.clearNotifications();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private clearNotifications(): void {
|
||||
this.props.onConsoleDataChange([]);
|
||||
}
|
||||
|
||||
private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] {
|
||||
return rowData.map((item: ConsoleData, index: number) => (
|
||||
<div className="rowData" key={index}>
|
||||
{item.type === ConsoleDataType.Info && <img className="infoIcon" src={InfoIcon} alt="info" />}
|
||||
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||
<span className="date">{item.date}</span>
|
||||
<span className="message">{item.message}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
||||
this.setState({ selectedFilter: event.target.value });
|
||||
}
|
||||
|
||||
private getFilteredConsoleData(): ConsoleData[] {
|
||||
let filterType: ConsoleDataType = null;
|
||||
|
||||
switch (this.state.selectedFilter) {
|
||||
case "All":
|
||||
filterType = null;
|
||||
break;
|
||||
case "In Progress":
|
||||
filterType = ConsoleDataType.InProgress;
|
||||
break;
|
||||
case "Info":
|
||||
filterType = ConsoleDataType.Info;
|
||||
break;
|
||||
case "Error":
|
||||
filterType = ConsoleDataType.Error;
|
||||
break;
|
||||
default:
|
||||
filterType = null;
|
||||
}
|
||||
|
||||
return filterType == null
|
||||
? this.props.consoleData
|
||||
: this.props.consoleData.filter((data: ConsoleData) => data.type === filterType);
|
||||
}
|
||||
|
||||
private setHeaderStatus(statusMessage: string): void {
|
||||
if (this.state.headerStatus === statusMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.headerTimeoutId && clearTimeout(this.headerTimeoutId);
|
||||
this.setState({ headerStatus: statusMessage });
|
||||
this.headerTimeoutId = window.setTimeout(
|
||||
() => this.setState({ headerStatus: "" }),
|
||||
ClientDefaults.errorNotificationTimeoutMs
|
||||
);
|
||||
}
|
||||
|
||||
private static extractHeaderStatus(props: NotificationConsoleComponentProps) {
|
||||
if (props.consoleData && props.consoleData.length > 0) {
|
||||
return props.consoleData[0].message.split(":\n")[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
this.props.onConsoleExpandedChange(this.state.isExpanded);
|
||||
if (this.state.isExpanded) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { NotificationConsoleComponent } from "./NotificationConsoleComponent";
|
||||
import { ConsoleData } from "./NotificationConsoleComponent";
|
||||
|
||||
export class NotificationConsoleComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
public container: ViewModels.Explorer;
|
||||
private consoleData: ko.ObservableArray<ConsoleData>;
|
||||
|
||||
constructor(container: ViewModels.Explorer) {
|
||||
this.container = container;
|
||||
|
||||
this.consoleData = container.notificationConsoleData;
|
||||
this.consoleData.subscribe((newValue: ConsoleData[]) => this.triggerRender());
|
||||
container.isNotificationConsoleExpanded.subscribe(() => this.triggerRender());
|
||||
this.parameters = ko.observable(Date.now());
|
||||
}
|
||||
|
||||
private onConsoleExpandedChange(isExpanded: boolean): void {
|
||||
isExpanded ? this.container.expandConsole() : this.container.collapseConsole();
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
private onConsoleDataChange(consoleData: ConsoleData[]): void {
|
||||
this.consoleData(consoleData);
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return (
|
||||
<NotificationConsoleComponent
|
||||
isConsoleExpanded={this.container.isNotificationConsoleExpanded()}
|
||||
onConsoleExpandedChange={this.onConsoleExpandedChange.bind(this)}
|
||||
consoleData={this.consoleData()}
|
||||
onConsoleDataChange={this.onConsoleDataChange.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private triggerRender() {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
||||
<div
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="statusBar"
|
||||
>
|
||||
<span
|
||||
className="dataTypeIcons"
|
||||
>
|
||||
<span
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
src=""
|
||||
/>
|
||||
<span
|
||||
className="numInProgress"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
src=""
|
||||
/>
|
||||
<span
|
||||
className="numErroredItems"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
src=""
|
||||
/>
|
||||
<span
|
||||
className="numInfoItems"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="consoleSplitter"
|
||||
/>
|
||||
<span
|
||||
className="headerStatus"
|
||||
>
|
||||
<span
|
||||
className="headerStatusEllipsis"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="collapse console"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
animateOpacity={false}
|
||||
animationStateClasses={
|
||||
Object {
|
||||
"animating": "rah-animating",
|
||||
"animatingDown": "rah-animating--down",
|
||||
"animatingToHeightAuto": "rah-animating--to-height-auto",
|
||||
"animatingToHeightSpecific": "rah-animating--to-height-specific",
|
||||
"animatingToHeightZero": "rah-animating--to-height-zero",
|
||||
"animatingUp": "rah-animating--up",
|
||||
"static": "rah-static",
|
||||
"staticHeightAuto": "rah-static--height-auto",
|
||||
"staticHeightSpecific": "rah-static--height-specific",
|
||||
"staticHeightZero": "rah-static--height-zero",
|
||||
}
|
||||
}
|
||||
applyInlineTransitions={true}
|
||||
delay={0}
|
||||
duration={200}
|
||||
easing="ease"
|
||||
height="auto"
|
||||
onAnimationEnd={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
className="notificationConsoleContents"
|
||||
>
|
||||
<div
|
||||
className="notificationConsoleControls"
|
||||
>
|
||||
<label
|
||||
id="consoleFilterLabel"
|
||||
>
|
||||
Filter
|
||||
</label>
|
||||
<select
|
||||
aria-label="All"
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
onChange={[Function]}
|
||||
role="combobox"
|
||||
value="All"
|
||||
>
|
||||
<option
|
||||
key="All"
|
||||
value="All"
|
||||
>
|
||||
All
|
||||
</option>
|
||||
<option
|
||||
key="In Progress"
|
||||
value="In Progress"
|
||||
>
|
||||
In Progress
|
||||
</option>
|
||||
<option
|
||||
key="Info"
|
||||
value="Info"
|
||||
>
|
||||
Info
|
||||
</option>
|
||||
<option
|
||||
key="Error"
|
||||
value="Error"
|
||||
>
|
||||
Error
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
className="consoleSplitter"
|
||||
/>
|
||||
<span
|
||||
className="clearNotificationsButton"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="clear notifications image"
|
||||
src=""
|
||||
/>
|
||||
Clear Notifications
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="notificationConsoleData"
|
||||
>
|
||||
<div
|
||||
className="rowData"
|
||||
key="0"
|
||||
>
|
||||
<img
|
||||
alt="info"
|
||||
className="infoIcon"
|
||||
src=""
|
||||
/>
|
||||
<span
|
||||
className="date"
|
||||
>
|
||||
date
|
||||
</span>
|
||||
<span
|
||||
className="message"
|
||||
>
|
||||
message
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
`;
|
||||
Reference in New Issue
Block a user