mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Move delete collection confirmation pane to react (#417)
This commit is contained in:
@@ -1,142 +0,0 @@
|
||||
jest.mock("../../Common/dataAccess/deleteCollection");
|
||||
import * as ko from "knockout";
|
||||
import * as sinon from "sinon";
|
||||
import Q from "q";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import DeleteCollectionConfirmationPane from "./DeleteCollectionConfirmationPane";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import Explorer from "../Explorer";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { TreeNode } from "../../Contracts/ViewModels";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
let database = {} as ViewModels.Database;
|
||||
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false if if 1 database and 2 collection", () => {
|
||||
let database = {} as ViewModels.Database;
|
||||
database.collections = ko.observableArray<ViewModels.Collection>([
|
||||
{} as ViewModels.Collection,
|
||||
{} as ViewModels.Collection,
|
||||
]);
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if 2 database and 1 collection each", () => {
|
||||
let database = {} as ViewModels.Database;
|
||||
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
||||
let database2 = {} as ViewModels.Database;
|
||||
database2.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if 0 databases", () => {
|
||||
let database = {} as ViewModels.Database;
|
||||
explorer.databases = ko.observableArray<ViewModels.Database>();
|
||||
database.collections = ko.observableArray<ViewModels.Collection>();
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
let fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
let pane = new DeleteCollectionConfirmationPane({
|
||||
id: "deletecollectionconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: fakeExplorer,
|
||||
});
|
||||
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
expect(pane.shouldRecordFeedback()).toBe(true);
|
||||
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => true;
|
||||
expect(pane.shouldRecordFeedback()).toBe(false);
|
||||
|
||||
fakeExplorer.isLastCollection = () => false;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
expect(pane.shouldRecordFeedback()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit()", () => {
|
||||
let telemetryProcessorSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
(deleteCollection as jest.Mock).mockResolvedValue(undefined);
|
||||
telemetryProcessorSpy = sinon.spy(TelemetryProcessor, "trace");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
telemetryProcessorSpy.restore();
|
||||
});
|
||||
|
||||
it("it should log feedback if last collection and database is not shared", () => {
|
||||
let selectedCollectionId = "testCol";
|
||||
let fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.findSelectedCollection = () => {
|
||||
return {
|
||||
id: ko.observable<string>(selectedCollectionId),
|
||||
rid: "test",
|
||||
} as ViewModels.Collection;
|
||||
};
|
||||
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
const SubscriptionId = "testId";
|
||||
const AccountName = "testAccount";
|
||||
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
||||
id: SubscriptionId,
|
||||
name: AccountName,
|
||||
} as DataModels.DatabaseAccount);
|
||||
|
||||
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
|
||||
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
let pane = new DeleteCollectionConfirmationPane({
|
||||
id: "deletecollectionconfirmationpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
container: fakeExplorer as any,
|
||||
});
|
||||
pane.collectionIdConfirmation = ko.observable<string>(selectedCollectionId);
|
||||
const Feedback = "my feedback";
|
||||
pane.containerDeleteFeedback(Feedback);
|
||||
|
||||
return pane.submit().then(() => {
|
||||
expect(telemetryProcessorSpy.called).toBe(true);
|
||||
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
||||
expect(
|
||||
telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
174
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.tsx
Normal file
174
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
jest.mock("../../Common/dataAccess/deleteCollection");
|
||||
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
|
||||
import * as ko from "knockout";
|
||||
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { Collection, Database } from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import Explorer from "../Explorer";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { TreeNode } from "../../Contracts/ViewModels";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
|
||||
describe("Delete Collection Confirmation Pane", () => {
|
||||
describe("Explorer.isLastCollection()", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be false if if 1 database and 2 collection", () => {
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection, {} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if 2 database and 1 collection each", () => {
|
||||
const database = {} as Database;
|
||||
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
const database2 = {} as Database;
|
||||
database2.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||
explorer.databases = ko.observableArray<Database>([database, database2]);
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be false if 0 databases", () => {
|
||||
const database = {} as Database;
|
||||
explorer.databases = ko.observableArray<Database>();
|
||||
database.collections = ko.observableArray<Collection>();
|
||||
expect(explorer.isLastCollection()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
const fakeExplorer = new Explorer();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
};
|
||||
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||
|
||||
props.explorer.isLastCollection = () => true;
|
||||
props.explorer.isSelectedDatabaseShared = () => true;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
|
||||
props.explorer.isLastCollection = () => false;
|
||||
props.explorer.isSelectedDatabaseShared = () => false;
|
||||
wrapper.setProps(props);
|
||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit()", () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const selectedCollectionId = "testCol";
|
||||
const databaseId = "testDatabase";
|
||||
const fakeExplorer = {} as Explorer;
|
||||
fakeExplorer.findSelectedCollection = () => {
|
||||
return {
|
||||
id: ko.observable<string>(selectedCollectionId),
|
||||
databaseId,
|
||||
rid: "test",
|
||||
} as Collection;
|
||||
};
|
||||
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
||||
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
||||
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||
fakeExplorer.isLastCollection = () => true;
|
||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
name: "testDatabaseAccountName",
|
||||
properties: {
|
||||
cassandraEndpoint: "testEndpoint",
|
||||
},
|
||||
id: "testDatabaseAccountId",
|
||||
} as DatabaseAccount,
|
||||
defaultExperience: DefaultAccountExperienceType.DocumentDB,
|
||||
});
|
||||
(deleteCollection as jest.Mock).mockResolvedValue(undefined);
|
||||
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
};
|
||||
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
|
||||
});
|
||||
|
||||
it("should call delete collection", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmCollectionId")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedCollectionId } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("click");
|
||||
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("should record feedback", async () => {
|
||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||
wrapper
|
||||
.find("#confirmCollectionId")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: selectedCollectionId } });
|
||||
|
||||
expect(wrapper.exists("#deleteCollectionFeedbackInput")).toBe(true);
|
||||
const feedbackText = "Test delete collection feedback text";
|
||||
wrapper
|
||||
.find("#deleteCollectionFeedbackInput")
|
||||
.hostNodes()
|
||||
.simulate("change", { target: { value: feedbackText } });
|
||||
|
||||
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||
wrapper.find("#sidePanelOkButton").hostNodes().simulate("click");
|
||||
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
"testDatabaseAccountId",
|
||||
"testDatabaseAccountName",
|
||||
ApiKind.SQL,
|
||||
feedbackText
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
});
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
186
src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx
Normal file
186
src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as React from "react";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
import { Collection } from "../../Contracts/ViewModels";
|
||||
import { Text, TextField } from "office-ui-fabric-react";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { Areas } from "../../Common/Constants";
|
||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { PanelErrorComponent, PanelErrorProps } from "./PanelErrorComponent";
|
||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||
import Explorer from "../Explorer";
|
||||
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||
|
||||
export interface DeleteCollectionConfirmationPanelProps {
|
||||
explorer: Explorer;
|
||||
closePanel: () => void;
|
||||
openNotificationConsole: () => void;
|
||||
}
|
||||
|
||||
export interface DeleteCollectionConfirmationPanelState {
|
||||
formError: string;
|
||||
isExecuting: boolean;
|
||||
}
|
||||
|
||||
export class DeleteCollectionConfirmationPanel extends React.Component<
|
||||
DeleteCollectionConfirmationPanelProps,
|
||||
DeleteCollectionConfirmationPanelState
|
||||
> {
|
||||
private inputCollectionName: string;
|
||||
private deleteCollectionFeedback: string;
|
||||
|
||||
constructor(props: DeleteCollectionConfirmationPanelProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
formError: "",
|
||||
isExecuting: false,
|
||||
};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div className="panelContentContainer">
|
||||
<PanelErrorComponent {...this.getPanelErrorProps()} />
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the collection id</Text>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
onChange={(event, newInput?: string) => {
|
||||
this.inputCollectionName = newInput;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{this.shouldRecordFeedback() && (
|
||||
<div className="deleteCollectionFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this container?
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteCollectionFeedbackInput"
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
multiline
|
||||
rows={3}
|
||||
onChange={(event, newInput?: string) => {
|
||||
this.deleteCollectionFeedback = newInput;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PanelFooterComponent buttonLabel="OK" onOKButtonClicked={() => this.submit()} />
|
||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.state.isExecuting}>
|
||||
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getPanelErrorProps(): PanelErrorProps {
|
||||
if (this.state.formError) {
|
||||
return {
|
||||
isWarning: false,
|
||||
message: this.state.formError,
|
||||
showErrorDetails: true,
|
||||
openNotificationConsole: this.props.openNotificationConsole,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isWarning: true,
|
||||
showErrorDetails: false,
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
};
|
||||
}
|
||||
|
||||
private shouldRecordFeedback(): boolean {
|
||||
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
|
||||
}
|
||||
|
||||
public async submit(): Promise<void> {
|
||||
const collection = this.props.explorer.findSelectedCollection();
|
||||
|
||||
if (!collection || this.inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input collection name does not match the selected collection";
|
||||
this.setState({ formError: errorMessage });
|
||||
NotificationConsoleUtils.logConsoleError(`Error while deleting collection ${collection.id()}: ${errorMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ formError: "", isExecuting: true });
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
|
||||
databaseAccountName: userContext.databaseAccount?.name,
|
||||
defaultExperience: userContext.defaultExperience,
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: "Delete Collection",
|
||||
});
|
||||
|
||||
try {
|
||||
await deleteCollection(collection.databaseId, collection.id());
|
||||
|
||||
this.setState({ isExecuting: false });
|
||||
this.props.explorer.selectedNode(collection.database);
|
||||
this.props.explorer.tabsManager?.closeTabsByComparator(
|
||||
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||
);
|
||||
this.props.explorer.refreshAllDatabases();
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
databaseAccountName: userContext.databaseAccount?.name,
|
||||
defaultExperience: userContext.defaultExperience,
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: "Delete Collection",
|
||||
},
|
||||
startKey
|
||||
);
|
||||
|
||||
if (this.shouldRecordFeedback()) {
|
||||
const deleteFeedback = new DeleteFeedback(
|
||||
userContext.databaseAccount?.id,
|
||||
userContext.databaseAccount?.name,
|
||||
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
|
||||
this.deleteCollectionFeedback
|
||||
);
|
||||
|
||||
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||
});
|
||||
}
|
||||
|
||||
this.props.closePanel();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.setState({ formError: errorMessage, isExecuting: false });
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteCollection,
|
||||
{
|
||||
databaseAccountName: userContext.databaseAccount?.name,
|
||||
defaultExperience: userContext.defaultExperience,
|
||||
collectionId: collection.id(),
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: "Delete Collection",
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/Explorer/Panes/PanelComponent.less
Normal file
57
src/Explorer/Panes/PanelComponent.less
Normal file
@@ -0,0 +1,57 @@
|
||||
@import "../../../less/Common/Constants";
|
||||
|
||||
.panelContentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.panelMainContent {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
color: @BaseDark;
|
||||
font-size: @largeFontSize;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.panelWarningErrorContainer {
|
||||
background-color: @BaseLow;
|
||||
padding: @DefaultSpace;
|
||||
display: inline-flex;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.panelWarningIcon {
|
||||
font-size: @WarningErrorIconSize;
|
||||
width: @WarningErrorIconSize;
|
||||
margin: auto 0 auto @SmallSpace;
|
||||
color: @WarningIconColor;
|
||||
}
|
||||
|
||||
.panelErrorIcon {
|
||||
font-size: @WarningErrorIconSize;
|
||||
width: @WarningErrorIconSize;
|
||||
margin: auto 0 auto @SmallSpace;
|
||||
color: @ErrorIconColor;
|
||||
}
|
||||
|
||||
.panelWarningErrorDetailsLinkContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: @MediumSpace;
|
||||
|
||||
.paneErrorLink {
|
||||
cursor: pointer;
|
||||
font-size: @mediumFontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panelFooter button {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.deleteCollectionFeedback {
|
||||
margin-top: 12px;
|
||||
}
|
||||
41
src/Explorer/Panes/PanelContainerComponent.test.tsx
Normal file
41
src/Explorer/Panes/PanelContainerComponent.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { PanelContainerComponent, PanelContainerProps } from "./PanelContainerComponent";
|
||||
|
||||
describe("PaneContainerComponent test", () => {
|
||||
it("should render with panel content and header", () => {
|
||||
const panelContainerProps: PanelContainerProps = {
|
||||
headerText: "test",
|
||||
panelContent: <div></div>,
|
||||
isOpen: true,
|
||||
isConsoleExpanded: false,
|
||||
closePanel: undefined,
|
||||
};
|
||||
const wrapper = shallow(<PanelContainerComponent {...panelContainerProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render nothing if content is undefined", () => {
|
||||
const panelContainerProps: PanelContainerProps = {
|
||||
headerText: "test",
|
||||
panelContent: undefined,
|
||||
isOpen: true,
|
||||
isConsoleExpanded: false,
|
||||
closePanel: undefined,
|
||||
};
|
||||
const wrapper = shallow(<PanelContainerComponent {...panelContainerProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should be resize if notification console is expanded", () => {
|
||||
const panelContainerProps: PanelContainerProps = {
|
||||
headerText: "test",
|
||||
panelContent: <div></div>,
|
||||
isOpen: true,
|
||||
isConsoleExpanded: true,
|
||||
closePanel: undefined,
|
||||
};
|
||||
const wrapper = shallow(<PanelContainerComponent {...panelContainerProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
58
src/Explorer/Panes/PanelContainerComponent.tsx
Normal file
58
src/Explorer/Panes/PanelContainerComponent.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from "react";
|
||||
import { Panel, PanelType } from "office-ui-fabric-react";
|
||||
|
||||
export interface PanelContainerProps {
|
||||
headerText: string;
|
||||
panelContent: JSX.Element;
|
||||
isConsoleExpanded: boolean;
|
||||
isOpen: boolean;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export class PanelContainerComponent extends React.Component<PanelContainerProps> {
|
||||
private static readonly consoleHeaderHeight = 32;
|
||||
private static readonly consoleContentHeight = 220;
|
||||
|
||||
render(): JSX.Element {
|
||||
if (!this.props.panelContent) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
headerText={this.props.headerText}
|
||||
isOpen={this.props.isOpen}
|
||||
onDismiss={this.onDissmiss}
|
||||
isLightDismiss
|
||||
type={PanelType.custom}
|
||||
closeButtonAriaLabel="Close"
|
||||
customWidth="440px"
|
||||
headerClassName="panelHeader"
|
||||
styles={{
|
||||
navigation: { borderBottom: "1px solid #cccccc" },
|
||||
content: { padding: "24px 34px 20px 34px", height: "100%" },
|
||||
scrollableContent: { height: "100%" },
|
||||
}}
|
||||
style={{ height: this.getPanelHeight() }}
|
||||
>
|
||||
{this.props.panelContent}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => {
|
||||
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||
ev.preventDefault();
|
||||
} else {
|
||||
this.props.closePanel();
|
||||
}
|
||||
};
|
||||
|
||||
private getPanelHeight = (): string => {
|
||||
const consoleHeight = this.props.isConsoleExpanded
|
||||
? PanelContainerComponent.consoleContentHeight + PanelContainerComponent.consoleHeaderHeight
|
||||
: PanelContainerComponent.consoleHeaderHeight;
|
||||
const panelHeight = window.innerHeight - consoleHeight;
|
||||
return panelHeight + "px";
|
||||
};
|
||||
}
|
||||
29
src/Explorer/Panes/PanelErrorComponent.tsx
Normal file
29
src/Explorer/Panes/PanelErrorComponent.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { Icon, Text } from "office-ui-fabric-react";
|
||||
|
||||
export interface PanelErrorProps {
|
||||
message: string;
|
||||
isWarning: boolean;
|
||||
showErrorDetails: boolean;
|
||||
openNotificationConsole?: () => void;
|
||||
}
|
||||
|
||||
export const PanelErrorComponent: React.FunctionComponent<PanelErrorProps> = (props: PanelErrorProps): JSX.Element => (
|
||||
<div className="panelWarningErrorContainer">
|
||||
{props.isWarning ? (
|
||||
<Icon iconName="WarningSolid" className="panelWarningIcon" />
|
||||
) : (
|
||||
<Icon iconName="StatusErrorFull" className="panelErrorIcon" />
|
||||
)}
|
||||
<span className="panelWarningErrorDetailsLinkContainer">
|
||||
<Text className="panelWarningErrorMessage" variant="small">
|
||||
{props.message}
|
||||
</Text>
|
||||
{props.showErrorDetails && (
|
||||
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
|
||||
More details
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
15
src/Explorer/Panes/PanelFooterComponent.tsx
Normal file
15
src/Explorer/Panes/PanelFooterComponent.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { PrimaryButton } from "office-ui-fabric-react";
|
||||
|
||||
export interface PanelFooterProps {
|
||||
buttonLabel: string;
|
||||
onOKButtonClicked: () => void;
|
||||
}
|
||||
|
||||
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
|
||||
props: PanelFooterProps
|
||||
): JSX.Element => (
|
||||
<div className="panelFooter">
|
||||
<PrimaryButton id="sidePanelOkButton" text={props.buttonLabel} onClick={() => props.onOKButtonClicked()} />
|
||||
</div>
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = `
|
||||
<StyledPanelBase
|
||||
closeButtonAriaLabel="Close"
|
||||
customWidth="440px"
|
||||
headerClassName="panelHeader"
|
||||
headerText="test"
|
||||
isLightDismiss={true}
|
||||
isOpen={true}
|
||||
onDismiss={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "516px",
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"content": Object {
|
||||
"height": "100%",
|
||||
"padding": "24px 34px 20px 34px",
|
||||
},
|
||||
"navigation": Object {
|
||||
"borderBottom": "1px solid #cccccc",
|
||||
},
|
||||
"scrollableContent": Object {
|
||||
"height": "100%",
|
||||
},
|
||||
}
|
||||
}
|
||||
type={7}
|
||||
>
|
||||
<div />
|
||||
</StyledPanelBase>
|
||||
`;
|
||||
|
||||
exports[`PaneContainerComponent test should render nothing if content is undefined 1`] = `<Fragment />`;
|
||||
|
||||
exports[`PaneContainerComponent test should render with panel content and header 1`] = `
|
||||
<StyledPanelBase
|
||||
closeButtonAriaLabel="Close"
|
||||
customWidth="440px"
|
||||
headerClassName="panelHeader"
|
||||
headerText="test"
|
||||
isLightDismiss={true}
|
||||
isOpen={true}
|
||||
onDismiss={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": "736px",
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"content": Object {
|
||||
"height": "100%",
|
||||
"padding": "24px 34px 20px 34px",
|
||||
},
|
||||
"navigation": Object {
|
||||
"borderBottom": "1px solid #cccccc",
|
||||
},
|
||||
"scrollableContent": Object {
|
||||
"height": "100%",
|
||||
},
|
||||
}
|
||||
}
|
||||
type={7}
|
||||
>
|
||||
<div />
|
||||
</StyledPanelBase>
|
||||
`;
|
||||
Reference in New Issue
Block a user