Upgrade react-md, improve query explorer and improve more things
This commit is contained in:
Родитель
60f0739a20
Коммит
7ee71697fd
|
@ -9,5 +9,5 @@ interface KustoTable {
|
|||
DataType: string,
|
||||
ColumnType: string
|
||||
}[],
|
||||
Rows: any[][]
|
||||
Rows: {}[][]
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"react-leaflet": "^1.7.8",
|
||||
"react-leaflet-div-icon": "^1.1.0",
|
||||
"react-leaflet-markercluster": "^1.1.8",
|
||||
"react-md": "^1.0.18",
|
||||
"react-md": "^1.3.1",
|
||||
"react-render-html": "^0.1.6",
|
||||
"react-router": "3.0.0",
|
||||
"react-scripts-ts": "^2.6.0",
|
||||
|
@ -54,7 +54,8 @@
|
|||
"react-ace": "^5.0.1",
|
||||
"react-adal": "^0.4.17",
|
||||
"react-json-tree": "^0.10.9",
|
||||
"xhr-request": "^1.0.1"
|
||||
"xhr-request": "^1.0.1",
|
||||
"react-monaco-editor": "^0.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"css:build": "node-sass src/ -o src/",
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
To begin the development, run `npm start`.
|
||||
To create a production bundle, use `npm run build`.
|
||||
-->
|
||||
<script src="../node_modules/kusto-language-service/bridge.js"></script>
|
||||
<script src="../node_modules/kusto-language-service/kusto.javascript.client.js"></script>
|
||||
<!-- <script src="../node_modules/kusto-language-service/bridge.js"></script>
|
||||
<script src="../node_modules/kusto-language-service/kusto.javascript.client.js"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import alt, { AbstractActions } from '../alt';
|
||||
|
||||
import KustoClientApi from '../api/external/KustoClientApi';
|
||||
|
||||
interface IQueryExplorerActions {
|
||||
prepareExecuteQuery(query: string): any;
|
||||
updateQuery(query: string): any;
|
||||
updateResponse(response: KustoQueryResults): any;
|
||||
executeQuery(clusterName: string, databaseName: string, query: string): any;
|
||||
updateRenderType(newRenderType: string): any;
|
||||
}
|
||||
|
||||
class QueryExplorerActions extends AbstractActions implements IQueryExplorerActions {
|
||||
prepareExecuteQuery(query: string) {
|
||||
return { query };
|
||||
}
|
||||
|
||||
updateQuery(query: string) {
|
||||
return { query };
|
||||
}
|
||||
updateResponse(response: KustoQueryResults) {
|
||||
return { response };
|
||||
}
|
||||
|
||||
updateRenderType(newRenderType: string) {
|
||||
return { newRenderType };
|
||||
}
|
||||
|
||||
executeQuery(clusterName: string, databaseName: string, query: string) {
|
||||
this.prepareExecuteQuery(query);
|
||||
|
||||
let kustoClientApi = new KustoClientApi();
|
||||
|
||||
kustoClientApi.executeQuery(clusterName, databaseName, query)
|
||||
.then((value: KustoQueryResults) => {
|
||||
this.updateResponse(value);
|
||||
})
|
||||
.catch((reason: any) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const queryExplorerActions = alt.createActions<IQueryExplorerActions>(QueryExplorerActions);
|
||||
|
||||
export default queryExplorerActions;
|
|
@ -0,0 +1,31 @@
|
|||
import { getToken } from '../../utils/authorization';
|
||||
|
||||
export default class KustoClient {
|
||||
public async executeQuery(cluster: string, database: string, query: string): Promise<KustoQueryResults> {
|
||||
let aadToken: string = await getToken();
|
||||
|
||||
let kustoResponse = await fetch(`https://${cluster}.kusto.windows.net/v1/rest/query`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
'db': database,
|
||||
'csl': query
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Authorization': `Bearer ${aadToken}`,
|
||||
}
|
||||
});
|
||||
|
||||
// 1. In case request wasn't successful - throw
|
||||
if (kustoResponse.status > 300) {
|
||||
let responseContent = await kustoResponse.text();
|
||||
|
||||
throw `Failed to query Kusto. Status code: ${kustoResponse.status},
|
||||
response: ${responseContent}`;
|
||||
}
|
||||
|
||||
let kustoResponseData: KustoQueryResults = await kustoResponse.json();
|
||||
|
||||
return kustoResponseData;
|
||||
}
|
||||
}
|
|
@ -221,7 +221,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
this.setState({ fileName: value });
|
||||
}
|
||||
|
||||
onLoad(importedFileContent: any, uploadResult: string) {
|
||||
onLoad(importedFileContent: File, uploadResult: string, event: Event) {
|
||||
const { name, size, type, lastModifiedDate } = importedFileContent;
|
||||
this.setState({ fileName: name.substr(0, name.indexOf('.')), content: uploadResult });
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
this.setState({ importVisible: false });
|
||||
}
|
||||
|
||||
setFile(importedFileContent: string) {
|
||||
setFile(importedFileContent: File, event: Event) {
|
||||
this.setState({ importedFileContent });
|
||||
}
|
||||
|
||||
|
|
|
@ -70,16 +70,16 @@ export default class Navbar extends React.Component<any, any> {
|
|||
let navigationItems = [];
|
||||
let toolbarTitle = null;
|
||||
|
||||
// Add the query explorer item
|
||||
// Add the query (query explorer) item
|
||||
navigationItems.push(
|
||||
<ListItem
|
||||
key={1001}
|
||||
component={Link}
|
||||
href={'/queryExplorer'}
|
||||
active={'/queryExplorer' === pathname}
|
||||
href={'/query'}
|
||||
active={'/query' === pathname}
|
||||
leftIcon={<FontIcon>{'search'}</FontIcon>}
|
||||
tileClassName="md-list-tile--mini"
|
||||
primaryText={name || 'Dashboard'}
|
||||
primaryText={name || 'Visualization'}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import injectTooltip from 'react-md/lib/Tooltips';
|
||||
|
||||
const Tooltip = injectTooltip(
|
||||
({children, className, tooltip, ...props }) => (
|
||||
<div {...props} className={(className || '') + ' inline-rel-container'} style={{position: 'relative'}}>
|
||||
const Tooltip = injectTooltip<{ className?: string, children?: React.ReactNode,
|
||||
style?: React.CSSProperties, tooltip?: React.ReactNode,
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void; }>(
|
||||
({children, className, style, tooltip, ...props }) => (
|
||||
<div {...props} className={(className || '') + ' inline-rel-container'}
|
||||
style={[style, { position: 'relative'}]}>
|
||||
{tooltip}
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,9 @@ import injectTooltip from 'react-md/lib/Tooltips';
|
|||
// Material icons shouldn't have any other children other than the child string and
|
||||
// it gets converted into a span if the tooltip is added, so we add a container
|
||||
// around the two.
|
||||
const TooltipFontIcon = injectTooltip(({
|
||||
const TooltipFontIcon = injectTooltip<{ forceIconFontSize?: boolean, iconClassName?: string, className?: string,
|
||||
forceIconSize?: number, style?: any, iconStyle?: React.CSSProperties,
|
||||
children?: React.ReactNode, tooltip?: React.ReactNode }>(({
|
||||
children, iconClassName, className, tooltip, forceIconFontSize, forceIconSize, style, iconStyle, ...props }) => (
|
||||
|
||||
<div {...props} style={style} className={(className || '') + ' inline-rel-container'}>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import XLabels from './XLabels';
|
||||
import DataGrid from './DataGrid';
|
||||
|
|
|
@ -31,7 +31,7 @@ export interface ITableColumnProps {
|
|||
click?: string;
|
||||
color?: string;
|
||||
tooltip?: string;
|
||||
tooltipPosition?: string;
|
||||
tooltipPosition?: 'top' | 'right' | 'bottom' | 'left';
|
||||
}
|
||||
|
||||
export interface ITableProps extends IGenericProps {
|
||||
|
@ -185,7 +185,7 @@ export default class Table extends GenericComponent<ITableProps, ITableState> {
|
|||
title={title}
|
||||
className={hideBorders ? 'hide-borders' : ''}
|
||||
contentStyle={styles.autoscroll}>
|
||||
<DataTable plain={!checkboxes} data={checkboxes} className={className} baseId="pagination" responsive={false}>
|
||||
<DataTable plain={!checkboxes} className={className} baseId="pagination" responsive={false}>
|
||||
<TableHeader>
|
||||
<TableRow autoAdjust={false}>
|
||||
{cols.map((col, i) => (
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { DataSourcePlugin, IOptions } from '../DataSourcePlugin';
|
||||
import { getToken } from '../../../utils/authorization';
|
||||
import KustoConnection from '../../connections/kusto';
|
||||
import KustoClient from '../../../api/external/KustoClientApi';
|
||||
import * as KustoUtils from '../../../utils/kusto/kustoUtils';
|
||||
|
||||
let connectionType = new KustoConnection();
|
||||
|
||||
|
@ -17,6 +19,8 @@ export default class KustoQuery extends DataSourcePlugin<IQueryParams> {
|
|||
defaultProperty = 'values';
|
||||
connectionType = connectionType.type;
|
||||
|
||||
private kustoClient: KustoClient = new KustoClient();
|
||||
|
||||
/**
|
||||
* @param options - Options object
|
||||
* @param connections - List of available connections
|
||||
|
@ -58,39 +62,13 @@ export default class KustoQuery extends DataSourcePlugin<IQueryParams> {
|
|||
};
|
||||
|
||||
return (dispatch) => {
|
||||
getToken().then((token: string) => {
|
||||
fetch(`https://${clusterName}.kusto.windows.net/v1/rest/query`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
'db': databaseName,
|
||||
'csl': query,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
response.json().then((resultTables: KustoQueryResults) => {
|
||||
let parsedKustoResponse = this.mapAllTables(resultTables);
|
||||
|
||||
// Assign the result table
|
||||
returnedResults.values = parsedKustoResponse[0];
|
||||
|
||||
// Extracting calculated values
|
||||
if (typeof params.calculated === 'function') {
|
||||
let additionalValues = params.calculated(parsedKustoResponse[0]) || {};
|
||||
Object.assign(returnedResults, additionalValues);
|
||||
}
|
||||
|
||||
return dispatch(returnedResults);
|
||||
});
|
||||
})
|
||||
.catch((reason) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(reason);
|
||||
});
|
||||
});
|
||||
this.kustoClient.executeQuery(clusterName, databaseName, query)
|
||||
.then((resultTables: KustoQueryResults) => {
|
||||
return dispatch(KustoUtils.convertKustoResultsToJsonObjects(resultTables));
|
||||
})
|
||||
.catch((reason) =>
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(reason));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -108,28 +86,4 @@ export default class KustoQuery extends DataSourcePlugin<IQueryParams> {
|
|||
|
||||
private validateParams(params: IQueryParams): void {
|
||||
}
|
||||
|
||||
private mapAllTables(results: KustoQueryResults): {}[][] {
|
||||
if (!results || !results.Tables || !results.Tables.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results.Tables.map((table, idx) => this.mapTable(table));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the Kusto results array into JSON objects
|
||||
* @param table Results table to be mapped into JSON object
|
||||
*/
|
||||
private mapTable(table: KustoTable): Array<{}> {
|
||||
return table.Rows.map((rowValues, rowIdx) => {
|
||||
let row = {};
|
||||
|
||||
table.Columns.forEach((col, idx) => {
|
||||
row[col.ColumnName] = rowValues[idx];
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import NotFound from './pages/NotFound';
|
|||
import Home from './pages/Home';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Setup from './pages/Setup';
|
||||
import QueryExplorer from './scenes/QueryExplorer';
|
||||
|
||||
export default (
|
||||
<Route component={App}>
|
||||
|
@ -14,6 +15,7 @@ export default (
|
|||
<Route path="/dashboard" component={Dashboard} />
|
||||
<Route path="/dashboard/:id" component={Dashboard}/>
|
||||
<Route path="/setup" component={Setup} />
|
||||
<Route path="/query" component={QueryExplorer} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Route>
|
||||
);
|
|
@ -0,0 +1,71 @@
|
|||
import * as React from 'react';
|
||||
import TextField from 'react-md/lib/TextFields';
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
|
||||
import MonacoQueryEditor from './components/monacoQueryEditor';
|
||||
import QueryResultPreview from './components/queryResultPreview';
|
||||
import QueryExplorerActions from '../../actions/QueryExplorerActions';
|
||||
|
||||
import './QueryExplorerStyle.css';
|
||||
|
||||
export interface QueryExplorerProps {
|
||||
renderAs?: 'table';
|
||||
}
|
||||
|
||||
export default class QueryExplorer extends React.Component<QueryExplorerProps> {
|
||||
private queryText: string;
|
||||
|
||||
constructor(props: QueryExplorerProps) {
|
||||
super(props);
|
||||
|
||||
this.onQueryTextChanged = this.onQueryTextChanged.bind(this);
|
||||
this.onExecuteQuery = this.onExecuteQuery.bind(this);
|
||||
|
||||
this.queryText = 'MaQosSummary | limit 1000 | summarize count() by bin(TIMESTAMP, 1s)';
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="query-explorer-container">
|
||||
<div className="connection-container">
|
||||
<TextField
|
||||
id="cluster"
|
||||
label="Cluster:"
|
||||
lineDirection="center"
|
||||
className="md-cell--stretch"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id="database"
|
||||
label="Database:"
|
||||
lineDirection="center"
|
||||
className="md-cell--stretch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
primary
|
||||
raised
|
||||
label="Go"
|
||||
style={{ width: 100 }}
|
||||
onClick={this.onExecuteQuery}
|
||||
/>
|
||||
|
||||
<MonacoQueryEditor onChange={this.onQueryTextChanged}/>
|
||||
|
||||
<div style={{ height: '21px', backgroundColor: 'gray' }} />
|
||||
|
||||
<QueryResultPreview renderAs="timeline" />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onQueryTextChanged(value: string) {
|
||||
this.queryText = value;
|
||||
}
|
||||
|
||||
private onExecuteQuery() {
|
||||
QueryExplorerActions.executeQuery('kuskus', 'kuskus', this.queryText);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.query-explorer-container {
|
||||
height: 100vh;
|
||||
|
||||
& > .connection-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import MonacoEditor from 'react-monaco-editor';
|
||||
import * as monacoEditor from 'monaco-editor';
|
||||
|
||||
export interface MonacoQueryEditorProps {
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export default class MonacoQueryEditor extends React.Component<MonacoQueryEditorProps> {
|
||||
constructor(props: MonacoQueryEditorProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div style={{ height: '30%' }}>
|
||||
<MonacoEditor
|
||||
width="100%"
|
||||
language="kusto"
|
||||
theme="vs"
|
||||
value="MaQosSummary | limit 1000 | summarize count() by bin(TIMESTAMP, 1s)"
|
||||
options={this.getMonacoEditorSettings()}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getMonacoEditorSettings(): monacoEditor.editor.IEditorOptions {
|
||||
var minimapOptions: monacoEditor.editor.IEditorMinimapOptions = {
|
||||
enabled: false
|
||||
};
|
||||
|
||||
var scrollbarOptions: monacoEditor.editor.IEditorScrollbarOptions = {
|
||||
horizontal: 'Hidden',
|
||||
arrowSize: 30,
|
||||
useShadows: false
|
||||
};
|
||||
|
||||
var editorOptions: monacoEditor.editor.IEditorOptions = {
|
||||
minimap: minimapOptions,
|
||||
scrollbar: scrollbarOptions,
|
||||
lineNumbers: 'off',
|
||||
lineHeight: 19,
|
||||
// fontSize: 19,
|
||||
suggestFontSize: 13,
|
||||
dragAndDrop: false,
|
||||
occurrencesHighlight: false,
|
||||
selectionHighlight: false,
|
||||
renderIndentGuides: false,
|
||||
wordWrap: 'off',
|
||||
wordWrapColumn: 0,
|
||||
renderLineHighlight: 'none',
|
||||
automaticLayout: true // Auto resize whenever DOM is changing (e.g. zooming)
|
||||
};
|
||||
|
||||
return editorOptions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import MonacoQueryEditor from './MonacoQueryEditor';
|
||||
|
||||
export default MonacoQueryEditor;
|
|
@ -0,0 +1,67 @@
|
|||
import * as React from 'react';
|
||||
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
|
||||
|
||||
import TableVisual from './components/DataVisualization/TableVisual';
|
||||
import TimelineVisual from './components/DataVisualization/TimelineVisual';
|
||||
import QueryExplorerStore, { QueryExplorerState, QueryInformation } from '../../../../stores/QueryExplorerStore';
|
||||
|
||||
export interface QueryResultPreviewProps {
|
||||
queryResponse?: KustoQueryResults;
|
||||
renderAs?: 'table' | 'timeline';
|
||||
}
|
||||
|
||||
export interface QueryResultPreviewState {
|
||||
queryResponseInformation?: QueryInformation;
|
||||
}
|
||||
|
||||
export default class QueryResultPreview extends React.Component<QueryResultPreviewProps, QueryResultPreviewState> {
|
||||
|
||||
constructor(props: QueryResultPreviewProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
queryResponseInformation: {
|
||||
isLoading: false,
|
||||
query: null,
|
||||
renderAs: null,
|
||||
response: null
|
||||
}
|
||||
};
|
||||
|
||||
this.onQueriesExplorerStoreChange = this.onQueriesExplorerStoreChange.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
QueryExplorerStore.listen(this.onQueriesExplorerStoreChange);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div style={{ height: '40%' }}>
|
||||
{
|
||||
(this.state.queryResponseInformation &&
|
||||
this.state.queryResponseInformation.isLoading &&
|
||||
<div style={{ width: '100%', top: 130, left: 0 }}>
|
||||
<CircularProgress id="testerProgress" />
|
||||
</div>)
|
||||
}
|
||||
{
|
||||
(this.state.queryResponseInformation &&
|
||||
!this.state.queryResponseInformation.isLoading &&
|
||||
this.props.renderAs === 'table' &&
|
||||
<TableVisual queryResponse={this.state.queryResponseInformation.response} />)
|
||||
}
|
||||
{
|
||||
(this.state.queryResponseInformation &&
|
||||
!this.state.queryResponseInformation.isLoading &&
|
||||
this.props.renderAs === 'timeline' &&
|
||||
<TimelineVisual queryResponse={this.state.queryResponseInformation.response} />)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onQueriesExplorerStoreChange(queryExplorerStoreState: QueryExplorerState) {
|
||||
this.setState({ queryResponseInformation: queryExplorerStoreState.queryInformation });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import * as React from 'react';
|
||||
import DataTable from 'react-md/lib/DataTables/DataTable';
|
||||
import TableHeader from 'react-md/lib/DataTables/TableHeader';
|
||||
import TableBody from 'react-md/lib/DataTables/TableBody';
|
||||
import TableRow from 'react-md/lib/DataTables/TableRow';
|
||||
import TableColumn from 'react-md/lib/DataTables/TableColumn';
|
||||
|
||||
export interface TableVisualProps {
|
||||
queryResponse?: KustoQueryResults;
|
||||
}
|
||||
|
||||
export default class TableVisual extends React.Component<TableVisualProps> {
|
||||
constructor(props: TableVisualProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
// Map between the Kusto response to table view
|
||||
let mapResult = (response: KustoQueryResults) => {
|
||||
const result = response &&
|
||||
response.Tables &&
|
||||
response.Tables.length > 0 &&
|
||||
response.Tables[0].Rows || [];
|
||||
const rows = result.map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{_.map(val => (<TableColumn>{val}</TableColumn>))}
|
||||
</TableRow>
|
||||
));
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
// Map between
|
||||
let mapColumns = (response: KustoQueryResults) => {
|
||||
const result = response &&
|
||||
response.Tables &&
|
||||
response.Tables.length > 0 &&
|
||||
response.Tables[0].Columns || [];
|
||||
|
||||
const columns = (
|
||||
<TableRow>
|
||||
{result.map((column) => (
|
||||
<TableColumn>
|
||||
{column.ColumnName}
|
||||
</TableColumn>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'white',
|
||||
border: '1px',
|
||||
borderStyle: 'groove'
|
||||
}}>
|
||||
<DataTable plain>
|
||||
<TableHeader>
|
||||
{mapColumns(this.props.queryResponse)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mapResult(this.props.queryResponse)}
|
||||
</TableBody>
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
|
||||
import * as KustoUtils from '../../../../../../utils/kusto/kustoUtils';
|
||||
|
||||
export interface TimelineVisualProps {
|
||||
queryResponse?: KustoQueryResults;
|
||||
}
|
||||
|
||||
export default class TimelineVisual extends React.Component<TimelineVisualProps> {
|
||||
public render() {
|
||||
if (!this.isValidTimelineData(this.props.queryResponse)) {
|
||||
return (<div>d</div>);
|
||||
}
|
||||
const data = [
|
||||
{name: 'Page A', uv: 4000, pv: 2400, amt: 2400},
|
||||
{name: 'Page B', uv: 3000, pv: 1398, amt: 2210},
|
||||
{name: 'Page C', uv: 2000, pv: 9800, amt: 2290},
|
||||
{name: 'Page D', uv: 2780, pv: 3908, amt: 2000},
|
||||
{name: 'Page E', uv: 1890, pv: 4800, amt: 2181},
|
||||
{name: 'Page F', uv: 2390, pv: 3800, amt: 2500},
|
||||
{name: 'Page G', uv: 3490, pv: 4300, amt: 2100},
|
||||
];
|
||||
|
||||
// Convert kusto results to valid JSON
|
||||
let kustoResponseAsJsons = KustoUtils.convertKustoResultsToJsonObjects(this.props.queryResponse);
|
||||
let chartData = kustoResponseAsJsons[0];
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData}
|
||||
margin={{top: 5, right: 30, left: 20, bottom: 5}}>
|
||||
<XAxis dataKey="TIMESTAMP"/>
|
||||
<YAxis/>
|
||||
<CartesianGrid strokeDasharray="3 3"/>
|
||||
<Tooltip/>
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="count_" stroke="#8884d8" activeDot={{r: 8}}/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
private isValidTimelineData(queryResponse: KustoQueryResults): boolean {
|
||||
if (!queryResponse || !queryResponse.Tables || !queryResponse.Tables[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Timeline data must have one datetime
|
||||
let datetimeColumns = queryResponse.Tables[0].Columns
|
||||
.filter(column => column.ColumnType === 'datetime');
|
||||
|
||||
if (!datetimeColumns || datetimeColumns.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import QueryResultPreview from './QueryResultPreview';
|
||||
|
||||
export default QueryResultPreview;
|
|
@ -1,11 +1,3 @@
|
|||
import * as React from 'react';
|
||||
import QueryExplorer from './QueryExplorer';
|
||||
|
||||
export default class QueryExplorer extends React.Component {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
yalla
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default QueryExplorer;
|
|
@ -0,0 +1,56 @@
|
|||
import alt, { AbstractStoreModel } from '../alt';
|
||||
|
||||
import queryExplorerActions from '../actions/QueryExplorerActions';
|
||||
|
||||
export interface QueryInformation {
|
||||
query?: string;
|
||||
response?: KustoQueryResults;
|
||||
isLoading?: boolean;
|
||||
renderAs?: 'table' | 'timeline' | 'bars' | 'pie';
|
||||
}
|
||||
|
||||
export interface QueryExplorerState {
|
||||
queryInformation: QueryInformation;
|
||||
}
|
||||
|
||||
class QueryExplorerStore extends AbstractStoreModel<QueryExplorerState> implements QueryExplorerState {
|
||||
queryInformation: QueryInformation;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.queryInformation = {
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
// TODO - get the values from a cookie, else initialize it
|
||||
this.bindListeners({
|
||||
prepareExecuteQuery: queryExplorerActions.prepareExecuteQuery,
|
||||
updateQuery: queryExplorerActions.updateQuery,
|
||||
updateResponse: queryExplorerActions.updateResponse,
|
||||
updateRenderType: queryExplorerActions.updateRenderType
|
||||
});
|
||||
}
|
||||
|
||||
updateQuery(state: any) {
|
||||
this.queryInformation.query = state.query;
|
||||
}
|
||||
|
||||
updateResponse(state: any) {
|
||||
this.queryInformation.isLoading = false;
|
||||
this.queryInformation.response = state.response;
|
||||
}
|
||||
|
||||
updateRenderType(state: any) {
|
||||
this.queryInformation.renderAs = state.newRenderType;
|
||||
}
|
||||
|
||||
prepareExecuteQuery(state: any) {
|
||||
this.queryInformation.isLoading = true;
|
||||
}
|
||||
}
|
||||
|
||||
const queryExplorerStore = alt.createStore<QueryExplorerState>((QueryExplorerStore as AltJS.StoreModel<any>),
|
||||
'QueryExplorerStore');
|
||||
|
||||
export default queryExplorerStore;
|
|
@ -52,7 +52,7 @@ export function timespan(
|
|||
'P90D';
|
||||
|
||||
let granularity =
|
||||
state.selectedValue === '24 hours' ? '5m' :
|
||||
state.selectedValue === '24 hours' ? '1h' :
|
||||
state.selectedValue === '1 week' ? '1d' : '1d';
|
||||
|
||||
let result = {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Convert the given Kusto query result to a list of JSON object per table
|
||||
* @param results The Kusto response
|
||||
*/
|
||||
export function convertKustoResultsToJsonObjects(results: KustoQueryResults): {}[][] {
|
||||
if (!results || !results.Tables || !results.Tables.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results.Tables.map((table, idx) => mapTable(table));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the Kusto results array into JSON objects
|
||||
* @param table Results table to be mapped into JSON object
|
||||
*/
|
||||
function mapTable(table: KustoTable): Array<{}> {
|
||||
return table.Rows.map((rowValues, rowIdx) => {
|
||||
let row = {};
|
||||
|
||||
table.Columns.forEach((col, idx) => {
|
||||
row[col.ColumnName] = rowValues[idx];
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
}
|
122
client/yarn.lock
122
client/yarn.lock
|
@ -2,6 +2,13 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/runtime@^7.0.0-beta.42":
|
||||
version "7.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.47.tgz#273f5e71629e80f6cbcd7507503848615e59f7e0"
|
||||
dependencies:
|
||||
core-js "^2.5.3"
|
||||
regenerator-runtime "^0.11.1"
|
||||
|
||||
"@types/alt@^0.16.32":
|
||||
version "0.16.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/alt/-/alt-0.16.34.tgz#8097539b86912f24bf1841d5414a7408c98fc7b1"
|
||||
|
@ -535,7 +542,7 @@ babel-runtime@6.23.0:
|
|||
core-js "^2.4.0"
|
||||
regenerator-runtime "^0.10.0"
|
||||
|
||||
babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1:
|
||||
babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
||||
dependencies:
|
||||
|
@ -1194,6 +1201,10 @@ core-js@^2.5.0:
|
|||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086"
|
||||
|
||||
core-js@^2.5.3:
|
||||
version "2.5.6"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.6.tgz#0fe6d45bf3cac3ac364a9d72de7576f4eb221b9d"
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
|
@ -2032,6 +2043,18 @@ fbjs@0.1.0-alpha.7:
|
|||
promise "^7.0.3"
|
||||
whatwg-fetch "^0.9.0"
|
||||
|
||||
fbjs@^0.8.16:
|
||||
version "0.8.16"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
|
||||
dependencies:
|
||||
core-js "^1.0.0"
|
||||
isomorphic-fetch "^2.1.1"
|
||||
loose-envify "^1.0.0"
|
||||
object-assign "^4.1.0"
|
||||
promise "^7.1.1"
|
||||
setimmediate "^1.0.5"
|
||||
ua-parser-js "^0.7.9"
|
||||
|
||||
fbjs@^0.8.4, fbjs@^0.8.9:
|
||||
version "0.8.14"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c"
|
||||
|
@ -3806,6 +3829,10 @@ moment@^2.10.6, moment@^2.14.1, moment@^2.18.0:
|
|||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
monaco-editor@^0.13.1:
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.13.1.tgz#6b9ce20e4d1c945042d256825eb133cb23315a52"
|
||||
|
||||
monaco-kusto@^0.0.10:
|
||||
version "0.0.10"
|
||||
resolved "https://1essharedassets.pkgs.visualstudio.com/_packaging/Kusto/npm/registry/monaco-kusto/-/monaco-kusto-0.0.10.tgz#afa6d1c5525dd60a1b6b45873fb3f23ef531268b"
|
||||
|
@ -4837,6 +4864,14 @@ promise@7.1.1, promise@^7.0.3, promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@15.6.0:
|
||||
version "15.6.0"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8:
|
||||
version "15.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
|
||||
|
@ -4844,6 +4879,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8:
|
|||
fbjs "^0.8.9"
|
||||
loose-envify "^1.3.1"
|
||||
|
||||
prop-types@^15.6.0, prop-types@^15.6.1:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
|
||||
dependencies:
|
||||
fbjs "^0.8.16"
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
propagate@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481"
|
||||
|
@ -5069,13 +5112,13 @@ react-error-overlay@^1.0.8:
|
|||
settle-promise "1.0.0"
|
||||
source-map "0.5.6"
|
||||
|
||||
react-event-listener@^0.4.5:
|
||||
version "0.4.5"
|
||||
resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.4.5.tgz#e3e895a0970cf14ee8f890113af68197abf3d0b1"
|
||||
react-event-listener@^0.5.1:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.5.6.tgz#f9349fda4b7735fc6886ca403bdcfd6057e89ceb"
|
||||
dependencies:
|
||||
babel-runtime "^6.20.0"
|
||||
fbjs "^0.8.4"
|
||||
prop-types "^15.5.4"
|
||||
"@babel/runtime" "^7.0.0-beta.42"
|
||||
fbjs "^0.8.16"
|
||||
prop-types "^15.6.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-grid-layout@^0.14.7:
|
||||
|
@ -5116,19 +5159,26 @@ react-leaflet@^1.7.8:
|
|||
lodash-es "^4.0.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-md@^1.0.18:
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/react-md/-/react-md-1.0.19.tgz#45d46b240435a008218121b5db8b0af71cf63ae8"
|
||||
react-md@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-md/-/react-md-1.3.1.tgz#4423610cc191b5ce3a6e322ace75c3aba610ebe9"
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
invariant "^2.2.1"
|
||||
prop-types "^15.5.8"
|
||||
prop-types "15.6.0"
|
||||
react-motion "^0.5.0"
|
||||
react-prop-types "^0.4.0"
|
||||
react-swipeable-views "^0.12.1"
|
||||
react-transition-group "^1.1.3"
|
||||
react-swipeable-views "^0.12.8"
|
||||
react-transition-group "^1.2.1"
|
||||
resize-observer-polyfill "^1.4.2"
|
||||
|
||||
react-monaco-editor@^0.17.0:
|
||||
version "0.17.0"
|
||||
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.17.0.tgz#d8e79f797c5b7684079424693daec8ca1258bb51"
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
monaco-editor "^0.13.1"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
react-motion@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.0.tgz#1708fc2aee552900d21c1e6bed28346863e017b6"
|
||||
|
@ -5220,36 +5270,36 @@ react-smooth@0.1.20:
|
|||
raf "^3.2.0"
|
||||
react-addons-transition-group "^0.14.0 || ^15.0.0"
|
||||
|
||||
react-swipeable-views-core@^0.12.5:
|
||||
version "0.12.5"
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.5.tgz#7113f3a5c84f85042447a7c49e3f2a206eab7931"
|
||||
react-swipeable-views-core@^0.12.11:
|
||||
version "0.12.11"
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.11.tgz#3cf2b4daffbb36f9d69bd19bf5b2d5370b6b2c1b"
|
||||
dependencies:
|
||||
babel-runtime "^6.23.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-swipeable-views-utils@^0.12.5:
|
||||
version "0.12.5"
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.5.tgz#878de2108c8fb5160cc3122de10c3ff741a28801"
|
||||
react-swipeable-views-utils@^0.12.13:
|
||||
version "0.12.13"
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.13.tgz#fe102524180bf568f746e844c8d74b9cd3e7e0b8"
|
||||
dependencies:
|
||||
babel-runtime "^6.23.0"
|
||||
fbjs "^0.8.4"
|
||||
keycode "^2.1.7"
|
||||
prop-types "^15.5.4"
|
||||
react-event-listener "^0.4.5"
|
||||
react-swipeable-views-core "^0.12.5"
|
||||
prop-types "^15.6.0"
|
||||
react-event-listener "^0.5.1"
|
||||
react-swipeable-views-core "^0.12.11"
|
||||
|
||||
react-swipeable-views@^0.12.1:
|
||||
version "0.12.5"
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.5.tgz#ff43b35e05c56614bb63ab032769ec8fe9259b0e"
|
||||
react-swipeable-views@^0.12.8:
|
||||
version "0.12.13"
|
||||
resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.13.tgz#247442dbe14922efe5ad6fe0297599c817600bf9"
|
||||
dependencies:
|
||||
babel-runtime "^6.23.0"
|
||||
dom-helpers "^3.2.1"
|
||||
prop-types "^15.5.4"
|
||||
react-swipeable-views-core "^0.12.5"
|
||||
react-swipeable-views-utils "^0.12.5"
|
||||
react-swipeable-views-core "^0.12.11"
|
||||
react-swipeable-views-utils "^0.12.13"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-transition-group@^1.1.3, react-transition-group@^1.2.0:
|
||||
react-transition-group@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.0.tgz#b51fc921b0c3835a7ef7c571c79fc82c73e9204f"
|
||||
dependencies:
|
||||
|
@ -5259,6 +5309,16 @@ react-transition-group@^1.1.3, react-transition-group@^1.2.0:
|
|||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-transition-group@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6"
|
||||
dependencies:
|
||||
chain-function "^1.0.0"
|
||||
dom-helpers "^3.2.0"
|
||||
loose-envify "^1.3.1"
|
||||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
|
||||
react@^15.4.2:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df"
|
||||
|
@ -5393,6 +5453,10 @@ regenerator-runtime@^0.11.0:
|
|||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
|
||||
|
||||
regenerator-runtime@^0.11.1:
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
|
||||
regex-cache@^0.4.2:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
|
||||
|
|
|
@ -38,7 +38,7 @@ Or
|
|||
|
||||
# Examples
|
||||
|
||||
## Updading selected values in filter
|
||||
## Updating selected values in filter
|
||||
|
||||
```ts
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче