298 lines
9.3 KiB
TypeScript

import {
ContextualMenu,
DetailsList,
DetailsListLayoutMode,
DetailsRow,
FocusZone,
IButtonProps,
IColumn,
IconButton,
IContextualMenuProps,
IDetailsListProps,
IDetailsRowProps,
IObjectWithKey,
ISelectionZoneProps,
ITextField,
ITextFieldProps,
Selection,
SelectionMode,
SelectionZone,
TextField,
} from "@fluentui/react";
import * as React from "react";
import * as _ from "underscore";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import * as Constants from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { QueriesClient } from "../../../Common/QueriesClient";
import * as DataModels from "../../../Contracts/DataModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { useDialog } from "../Dialog";
const title = "Open Saved Queries";
export interface QueriesGridComponentProps {
queriesClient: QueriesClient;
onQuerySelect: (query: DataModels.Query) => void;
containerVisible: boolean;
saveQueryEnabled: boolean;
}
export interface QueriesGridComponentState {
queries: Query[];
filteredResults: Query[];
}
interface Query extends DataModels.Query, IObjectWithKey {
key: string;
}
export class QueriesGridComponent extends React.Component<QueriesGridComponentProps, QueriesGridComponentState> {
private selection: Selection;
private queryFilter: ITextField;
constructor(props: QueriesGridComponentProps) {
super(props);
this.state = {
queries: [],
filteredResults: [],
};
this.selection = new Selection();
this.selection.setItems(this.state.filteredResults);
}
public componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void {
this.selection.setItems(
this.state.filteredResults,
!_.isEqual(prevState.filteredResults, this.state.filteredResults)
);
this.queryFilter && this.queryFilter.focus();
const querySetupCompleted: boolean = !prevProps.saveQueryEnabled && this.props.saveQueryEnabled;
const noQueryFiltersApplied: boolean = !this.queryFilter || !this.queryFilter.value;
if (!this.props.containerVisible || !this.props.saveQueryEnabled) {
return;
} else if (noQueryFiltersApplied && (!prevProps.containerVisible || querySetupCompleted)) {
// refresh only when pane is opened or query setup was recently completed
this.fetchSavedQueries();
}
}
// fetched saved queries when panel open
public componentDidMount() {
this.fetchSavedQueries();
}
public render(): JSX.Element {
if (this.state.queries.length === 0) {
return this.renderBannerComponent();
}
return this.renderQueryGridComponent();
}
private renderQueryGridComponent(): JSX.Element {
const searchFilterProps: ITextFieldProps = {
placeholder: "Search for Queries",
ariaLabel: "Query filter input",
onChange: this.onFilterInputChange,
componentRef: (queryInput: ITextField) => (this.queryFilter = queryInput),
styles: {
root: { paddingBottom: "12px" },
field: { fontSize: `${StyleConstants.mediumFontSize}px` },
},
};
const selectionContainerProps: ISelectionZoneProps = {
selection: this.selection,
selectionMode: SelectionMode.single,
onItemInvoked: (item: Query) => this.props.onQuerySelect(item),
};
const detailsListProps: IDetailsListProps = {
items: this.state.filteredResults,
columns: this.getColumns(),
isHeaderVisible: false,
setKey: "queryName",
layoutMode: DetailsListLayoutMode.fixedColumns,
selection: this.selection,
selectionMode: SelectionMode.none,
compact: true,
onRenderRow: this.onRenderRow,
styles: {
root: { width: "100%" },
},
};
return (
<FocusZone style={{ width: "100%" }}>
<TextField {...searchFilterProps} />
<SelectionZone {...selectionContainerProps}>
<DetailsList {...detailsListProps} />
</SelectionZone>
</FocusZone>
);
}
private renderBannerComponent(): JSX.Element {
const bannerProps: React.ImgHTMLAttributes<HTMLImageElement> = {
src: SaveQueryBannerIcon,
alt: "Save query helper banner",
style: {
height: "150px",
width: "310px",
marginTop: "20px",
border: `1px solid ${StyleConstants.BaseMedium}`,
},
};
return (
<div id="emptyQueryBanner">
<div>
You have not saved any queries yet. <br /> <br />
To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save
Query and follow the prompt in order to save the query.
</div>
<img {...bannerProps} />
</div>
);
}
private onFilterInputChange = (event: React.FormEvent<HTMLInputElement>, query: string): void => {
if (query) {
const filteredQueries: Query[] = this.state.queries.filter(
(savedQuery: Query) =>
savedQuery.queryName.indexOf(query) > -1 || savedQuery.queryName.toLowerCase().indexOf(query) > -1
);
this.setState({
filteredResults: filteredQueries,
});
} else {
// no filter
this.setState({
filteredResults: this.state.queries,
});
}
};
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
props.styles = {
root: { width: "100%" },
fields: {
width: "100%",
justifyContent: "space-between",
},
cell: {
margin: "auto 0",
},
};
return <DetailsRow data-selection-invoke={true} {...props} />;
};
private getColumns(): IColumn[] {
return [
{
key: "Name",
name: "Name",
fieldName: "queryName",
minWidth: 260,
},
{
key: "Action",
name: "Action",
fieldName: undefined,
minWidth: 70,
onRender: (query: Query) => {
const buttonProps: IButtonProps = {
iconProps: {
iconName: "More",
title: "More",
ariaLabel: "More actions button",
},
menuIconProps: {
styles: { root: { display: "none" } },
},
menuProps: {
isBeakVisible: true,
items: [
{
key: "Open",
text: "Open query",
onClick: () => {
this.props.onQuerySelect(query);
},
},
{
key: "Delete",
text: "Delete query",
onClick: async () => {
useDialog.getState().showOkCancelModalDialog(
"Confirm delete",
"Are you sure you want to delete this query?",
"Delete",
async () => {
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
});
try {
await this.props.queriesClient.deleteQuery(query);
TelemetryProcessor.traceSuccess(
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
},
startKey
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.DeleteSavedQuery,
{
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: title,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
}
await this.fetchSavedQueries(); // get latest state
},
"Cancel",
undefined
);
},
},
],
},
menuAs: (menuProps: IContextualMenuProps): JSX.Element => {
return <ContextualMenu {...menuProps} />;
},
};
return <IconButton {...buttonProps} />;
},
},
];
}
private async fetchSavedQueries(): Promise<void> {
let queries: Query[];
try {
queries = (await this.props.queriesClient.getQueries()) as Query[];
} catch (error) {
console.error(error);
return;
}
queries = queries.map((query: Query) => {
query.key = query.queryName;
return query;
});
// we do a deep equality check before setting the state to avoid infinite re-renders
if (!_.isEqual(queries, this.state.queries)) {
this.setState({
filteredResults: queries,
queries: queries,
});
}
}
}