Merge branch 'master' into dashboard-react-page

This commit is contained in:
Torkel Ödegaard 2019-02-05 17:56:04 +01:00
commit fa32198831
46 changed files with 1328 additions and 1020 deletions

View File

@ -3,6 +3,7 @@
### Minor
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
* **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson)
* **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182)
# 6.0.0-beta1 (2019-01-30)

View File

@ -393,9 +393,7 @@ Analytics ID here. By default this feature is disabled.
### check_for_updates
Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
send any sensitive information.
Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
<hr />

View File

@ -1,6 +1,6 @@
import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel';
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
/**
@ -41,6 +41,12 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
pluginExports?: PluginExports;
}
export interface ExploreDataSourceApi<TQuery extends DataQuery = DataQuery> extends DataSourceApi {
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
getHighlighterExpression?(query: TQuery): string;
languageProvider?: any;
}
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
datasource: DSType;
query: TQuery;
@ -48,15 +54,30 @@ export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends D
onChange: (value: TQuery) => void;
}
export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
datasource: DSType;
query: TQuery;
error?: string | JSX.Element;
hint?: QueryHint;
history: any[];
onExecuteQuery?: () => void;
onQueryChange?: (value: TQuery) => void;
onExecuteHint?: (action: QueryFixAction) => void;
}
export interface ExploreStartPageProps {
onClickExample: (query: DataQuery) => void;
}
export interface PluginExports {
Datasource?: DataSourceApi;
QueryCtrl?: any;
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, DataQuery>>;
ConfigCtrl?: any;
AnnotationsQueryCtrl?: any;
VariableQueryEditor?: any;
ExploreQueryField?: any;
ExploreStartPage?: any;
ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, DataQuery>>;
ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
// Panel plugin
PanelCtrl?: any;
@ -114,5 +135,3 @@ export interface PluginMetaInfo {
updated: string;
version: string;
}

View File

@ -11,7 +11,7 @@ import { colors } from '@grafana/ui';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
// Types
import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types';
import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui/src/types';
import TimeSeries from 'app/core/time_series2';
import {
ExploreUrlState,
@ -336,3 +336,12 @@ export function clearHistory(datasourceId: string) {
const historyKey = `grafana.explore.history.${datasourceId}`;
store.delete(historyKey);
}
export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => {
const queryKeys = queries.reduce((newQueryKeys, query, index) => {
const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
return newQueryKeys.concat(`${primaryKey}-${index}`);
}, []);
return queryKeys;
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AddPanelWidget, Props } from './AddPanelWidget';
import { DashboardModel, PanelModel } from '../../state';
const setup = (propOverrides?: object) => {
const props: Props = {
dashboard: {} as DashboardModel,
panel: {} as PanelModel,
};
Object.assign(props, propOverrides);
return shallow(<AddPanelWidget {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,12 +1,20 @@
// Libraries
import React from 'react';
import _ from 'lodash';
// Utils
import config from 'app/core/config';
import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import store from 'app/core/store';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { updateLocation } from 'app/core/actions';
// Store
import { store as reduxStore } from 'app/store/store';
import { updateLocation } from 'app/core/actions';
// Types
import { PanelModel } from '../../state';
import { DashboardModel } from '../../state';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { LocationUpdate } from 'app/types';
export interface Props {
panel: PanelModel;
@ -46,6 +54,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
copiedPanels.push(pluginCopy);
}
}
return _.sortBy(copiedPanels, 'sort');
}
@ -54,28 +63,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
}
copyButton(panel) {
return (
<button className="btn-inverse btn" onClick={() => this.onPasteCopiedPanel(panel)} title={panel.name}>
Paste copied Panel
</button>
);
}
moveToEdit(panel) {
reduxStore.dispatch(
updateLocation({
query: {
panelId: panel.id,
edit: true,
fullscreen: true,
},
partial: true,
})
);
}
onCreateNewPanel = () => {
onCreateNewPanel = (tab = 'queries') => {
const dashboard = this.props.dashboard;
const { gridPos } = this.props.panel;
@ -88,7 +76,21 @@ export class AddPanelWidget extends React.Component<Props, State> {
dashboard.addPanel(newPanel);
dashboard.removePanel(this.props.panel);
this.moveToEdit(newPanel);
const location: LocationUpdate = {
query: {
panelId: newPanel.id,
edit: true,
fullscreen: true,
},
partial: true,
};
if (tab === 'visualization') {
location.query.tab = 'visualization';
location.query.openVizPicker = true;
}
reduxStore.dispatch(updateLocation(location));
};
onPasteCopiedPanel = panelPluginInfo => {
@ -125,30 +127,50 @@ export class AddPanelWidget extends React.Component<Props, State> {
dashboard.removePanel(this.props.panel);
};
render() {
let addCopyButton;
renderOptionLink = (icon, text, onClick) => {
return (
<div>
<a href="#" onClick={onClick} className="add-panel-widget__link btn btn-inverse">
<div className="add-panel-widget__icon">
<i className={`gicon gicon-${icon}`} />
</div>
<span>{text}</span>
</a>
</div>
);
};
if (this.state.copiedPanelPlugins.length === 1) {
addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]);
}
render() {
const { copiedPanelPlugins } = this.state;
return (
<div className="panel-container add-panel-widget-container">
<div className="add-panel-widget">
<div className="add-panel-widget__header grid-drag-handle">
<i className="gicon gicon-add-panel" />
<span className="add-panel-widget__title">New Panel</span>
<button className="add-panel-widget__close" onClick={this.handleCloseAddPanel}>
<i className="fa fa-close" />
</button>
</div>
<div className="add-panel-widget__btn-container">
<button className="btn-success btn btn-large" onClick={this.onCreateNewPanel}>
Edit Panel
</button>
{addCopyButton}
<button className="btn-inverse btn" onClick={this.onCreateNewRow}>
Add Row
</button>
<div className="add-panel-widget__create">
{this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)}
{this.renderOptionLink('visualization', 'Choose Visualization', () =>
this.onCreateNewPanel('visualization')
)}
</div>
<div className="add-panel-widget__actions">
<button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>Convert to row</button>
{copiedPanelPlugins.length === 1 && (
<button
className="btn btn-inverse add-panel-widget__action"
onClick={() => this.onPasteCopiedPanel(copiedPanelPlugins[0])}
>
Paste copied panel
</button>
)}
</div>
</div>
</div>
</div>

View File

@ -14,6 +14,9 @@
align-items: center;
width: 100%;
cursor: move;
background: $page-header-bg;
box-shadow: $page-header-shadow;
border-bottom: 1px solid $page-header-border-color;
.gicon {
font-size: 30px;
@ -26,6 +29,29 @@
}
}
.add-panel-widget__title {
font-size: $font-size-md;
font-weight: $font-weight-semi-bold;
margin-right: $spacer*2;
}
.add-panel-widget__link {
margin: 0 8px;
width: 154px;
}
.add-panel-widget__icon {
margin-bottom: 8px;
.gicon {
color: white;
height: 44px;
width: 53px;
position: relative;
left: 5px;
}
}
.add-panel-widget__close {
margin-left: auto;
background-color: transparent;
@ -34,14 +60,25 @@
margin-right: -10px;
}
.add-panel-widget__create {
display: inherit;
margin-bottom: 24px;
// this is to have the big button appear centered
margin-top: 55px;
}
.add-panel-widget__actions {
display: inherit;
}
.add-panel-widget__action {
margin: 0 4px;
}
.add-panel-widget__btn-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
.btn {
margin-bottom: 10px;
}
}

View File

@ -0,0 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="panel-container add-panel-widget-container"
>
<div
className="add-panel-widget"
>
<div
className="add-panel-widget__header grid-drag-handle"
>
<i
className="gicon gicon-add-panel"
/>
<span
className="add-panel-widget__title"
>
New Panel
</span>
<button
className="add-panel-widget__close"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
</div>
<div
className="add-panel-widget__btn-container"
>
<div
className="add-panel-widget__create"
>
<div>
<a
className="add-panel-widget__link btn btn-inverse"
href="#"
onClick={[Function]}
>
<div
className="add-panel-widget__icon"
>
<i
className="gicon gicon-queries"
/>
</div>
<span>
Add Query
</span>
</a>
</div>
<div>
<a
className="add-panel-widget__link btn btn-inverse"
href="#"
onClick={[Function]}
>
<div
className="add-panel-widget__icon"
>
<i
className="gicon gicon-visualization"
/>
</div>
<span>
Choose Visualization
</span>
</a>
</div>
</div>
<div
className="add-panel-widget__actions"
>
<button
className="btn btn-inverse add-panel-widget__action"
onClick={[Function]}
>
Convert to row
</button>
</div>
</div>
</div>
</div>
`;

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { QueriesTab } from './QueriesTab';
import { VisualizationTab } from './VisualizationTab';
import VisualizationTab from './VisualizationTab';
import { GeneralTab } from './GeneralTab';
import { AlertTab } from '../../alerting/AlertTab';
@ -38,7 +38,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
onChangeTab = (tab: PanelEditorTab) => {
store.dispatch(
updateLocation({
query: { tab: tab.id },
query: { tab: tab.id, openVizPicker: null },
partial: true,
})
);

View File

@ -3,6 +3,9 @@ import React, { PureComponent } from 'react';
// Utils & Services
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { StoreState } from 'app/types';
import { updateLocation } from 'app/core/actions';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
@ -21,6 +24,8 @@ interface Props {
plugin: PanelPlugin;
angularPanel?: AngularComponent;
onTypeChanged: (newType: PanelPlugin) => void;
updateLocation: typeof updateLocation;
urlOpenVizPicker: boolean;
}
interface State {
@ -38,7 +43,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
super(props);
this.state = {
isVizPickerOpen: false,
isVizPickerOpen: this.props.urlOpenVizPicker,
searchQuery: '',
scrollTop: 0,
};
@ -149,6 +154,10 @@ export class VisualizationTab extends PureComponent<Props, State> {
};
onCloseVizPicker = () => {
if (this.props.urlOpenVizPicker) {
this.props.updateLocation({ query: { openVizPicker: null }, partial: true });
}
this.setState({ isVizPickerOpen: false });
};
@ -236,3 +245,13 @@ export class VisualizationTab extends PureComponent<Props, State> {
);
}
}
const mapStateToProps = (state: StoreState) => ({
urlOpenVizPicker: !!state.location.query.openVizPicker
});
const mapDispatchToProps = {
updateLocation
};
export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps);

View File

@ -1,5 +1,5 @@
// Libraries
import React from 'react';
import React, { ComponentClass } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import _ from 'lodash';
@ -18,34 +18,26 @@ import TableContainer from './TableContainer';
import TimePicker, { parseTime } from './TimePicker';
// Actions
import {
changeSize,
changeTime,
initializeExplore,
modifyQueries,
scanStart,
scanStop,
setQueries,
} from './state/actions';
import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions';
// Types
import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui';
import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui';
import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore';
import { StoreState } from 'app/types';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter';
import { ExploreToolbar } from './ExploreToolbar';
import { scanStopAction } from './state/actionTypes';
interface ExploreProps {
StartPage?: any;
StartPage?: ComponentClass<ExploreStartPageProps>;
changeSize: typeof changeSize;
changeTime: typeof changeTime;
datasourceError: string;
datasourceInstance: any;
datasourceInstance: ExploreDataSourceApi;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
exploreId: ExploreId;
initialQueries: DataQuery[];
initializeExplore: typeof initializeExplore;
initialized: boolean;
modifyQueries: typeof modifyQueries;
@ -54,14 +46,15 @@ interface ExploreProps {
scanning?: boolean;
scanRange?: RawTimeRange;
scanStart: typeof scanStart;
scanStop: typeof scanStop;
scanStopAction: typeof scanStopAction;
setQueries: typeof setQueries;
split: boolean;
showingStartPage?: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
urlState?: ExploreUrlState;
urlState: ExploreUrlState;
queryKeys: string[];
}
/**
@ -173,7 +166,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
};
onStopScanning = () => {
this.props.scanStop(this.props.exploreId);
this.props.scanStopAction({ exploreId: this.props.exploreId });
};
render() {
@ -184,12 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps> {
datasourceLoading,
datasourceMissing,
exploreId,
initialQueries,
showingStartPage,
split,
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
} = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore';
@ -210,7 +203,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
{datasourceInstance &&
!datasourceError && (
<div className="explore-container">
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} initialQueries={initialQueries} />
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
<AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => (
<main className="m-t-2" style={{ width }}>
@ -252,13 +245,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
datasourceInstance,
datasourceLoading,
datasourceMissing,
initialQueries,
initialized,
range,
showingStartPage,
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
} = item;
return {
StartPage,
@ -266,7 +259,6 @@ function mapStateToProps(state: StoreState, { exploreId }) {
datasourceInstance,
datasourceLoading,
datasourceMissing,
initialQueries,
initialized,
range,
showingStartPage,
@ -274,6 +266,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
supportsGraph,
supportsLogs,
supportsTable,
queryKeys,
};
}
@ -283,7 +276,7 @@ const mapDispatchToProps = {
initializeExplore,
modifyQueries,
scanStart,
scanStop,
scanStopAction,
setQueries,
};

View File

@ -8,6 +8,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store';
import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions';
import TimePicker from './TimePicker';
import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
enum IconSide {
left = 'left',
@ -79,6 +80,10 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
this.props.runQuery(this.props.exploreId);
};
onCloseTimePicker = () => {
this.props.timepickerRef.current.setState({ isOpen: false });
};
render() {
const {
datasourceMissing,
@ -137,7 +142,9 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
</div>
) : null}
<div className="explore-toolbar-content-item timepicker">
<TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
<TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
</ClickOutsideWrapper>
</div>
<div className="explore-toolbar-content-item">
<button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>

View File

@ -14,7 +14,7 @@ interface QueryEditorProps {
datasource: any;
error?: string | JSX.Element;
onExecuteQuery?: () => void;
onQueryChange?: (value: DataQuery, override?: boolean) => void;
onQueryChange?: (value: DataQuery) => void;
initialQuery: DataQuery;
exploreEvents: Emitter;
range: RawTimeRange;
@ -40,20 +40,17 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
datasource,
target,
refresh: () => {
this.props.onQueryChange(target, false);
this.props.onQueryChange(target);
this.props.onExecuteQuery();
},
events: exploreEvents,
panel: {
datasource,
targets: [target],
},
panel: { datasource, targets: [target] },
dashboard: {},
},
};
this.component = loader.load(this.element, scopeProps, template);
this.props.onQueryChange(target, false);
this.props.onQueryChange(target);
}
componentWillUnmount() {

View File

@ -33,10 +33,9 @@ export interface QueryFieldProps {
cleanText?: (text: string) => string;
disabled?: boolean;
initialQuery: string | null;
onBlur?: () => void;
onFocus?: () => void;
onExecuteQuery?: () => void;
onQueryChange?: (value: string) => void;
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
onValueChanged?: (value: string) => void;
onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
placeholder?: string;
portalOrigin?: string;
@ -51,6 +50,7 @@ export interface QueryFieldState {
typeaheadPrefix: string;
typeaheadText: string;
value: Value;
lastExecutedValue: Value;
}
export interface TypeaheadInput {
@ -90,6 +90,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
typeaheadPrefix: '',
typeaheadText: '',
value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
lastExecutedValue: null,
};
}
@ -132,11 +133,11 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
if (this.placeholdersBuffer.hasPlaceholders()) {
change.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
this.onChange(change);
this.onChange(change, true);
}
}
onChange = ({ value }) => {
onChange = ({ value }, invokeParentOnValueChanged?: boolean) => {
const documentChanged = value.document !== this.state.value.document;
const prevValue = this.state.value;
@ -144,8 +145,8 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
this.setState({ value }, () => {
if (documentChanged) {
const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value);
if (textChanged) {
this.handleChangeValue();
if (textChanged && invokeParentOnValueChanged) {
this.executeOnQueryChangeAndExecuteQueries();
}
}
});
@ -159,11 +160,16 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
}
};
handleChangeValue = () => {
executeOnQueryChangeAndExecuteQueries = () => {
// Send text change to parent
const { onValueChanged } = this.props;
if (onValueChanged) {
onValueChanged(Plain.serialize(this.state.value));
const { onQueryChange, onExecuteQuery } = this.props;
if (onQueryChange) {
onQueryChange(Plain.serialize(this.state.value));
}
if (onExecuteQuery) {
onExecuteQuery();
this.setState({ lastExecutedValue: this.state.value });
}
};
@ -288,8 +294,37 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
.focus();
}
onKeyDown = (event, change) => {
handleEnterAndTabKey = change => {
const { typeaheadIndex, suggestions } = this.state;
if (this.menuEl) {
// Dont blur input
event.preventDefault();
if (!suggestions || suggestions.length === 0) {
return undefined;
}
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
const nextChange = this.applyTypeahead(change, suggestion);
const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text');
if (insertTextOperation) {
const suggestionText = insertTextOperation.text;
this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
if (this.placeholdersBuffer.hasPlaceholders()) {
nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
}
return true;
} else {
this.executeOnQueryChangeAndExecuteQueries();
return undefined;
}
};
onKeyDown = (event, change) => {
const { typeaheadIndex } = this.state;
switch (event.key) {
case 'Escape': {
@ -312,27 +347,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
}
case 'Enter':
case 'Tab': {
if (this.menuEl) {
// Dont blur input
event.preventDefault();
if (!suggestions || suggestions.length === 0) {
return undefined;
}
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
const nextChange = this.applyTypeahead(change, suggestion);
const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text');
if (insertTextOperation) {
const suggestionText = insertTextOperation.text;
this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
if (this.placeholdersBuffer.hasPlaceholders()) {
nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
}
return true;
}
return this.handleEnterAndTabKey(change);
break;
}
@ -364,39 +379,33 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
resetTypeahead = () => {
if (this.mounted) {
this.setState({
suggestions: [],
typeaheadIndex: 0,
typeaheadPrefix: '',
typeaheadContext: null,
});
this.setState({ suggestions: [], typeaheadIndex: 0, typeaheadPrefix: '', typeaheadContext: null });
this.resetTimer = null;
}
};
handleBlur = () => {
const { onBlur } = this.props;
handleBlur = (event, change) => {
const { lastExecutedValue } = this.state;
const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null;
const currentValue = Plain.serialize(change.value);
// If we dont wait here, menu clicks wont work because the menu
// will be gone.
this.resetTimer = setTimeout(this.resetTypeahead, 100);
// Disrupting placeholder entry wipes all remaining placeholders needing input
this.placeholdersBuffer.clearPlaceholders();
if (onBlur) {
onBlur();
if (previousValue !== currentValue) {
this.executeOnQueryChangeAndExecuteQueries();
}
};
handleFocus = () => {
const { onFocus } = this.props;
if (onFocus) {
onFocus();
}
};
handleFocus = () => {};
onClickMenu = (item: CompletionItem) => {
// Manually triggering change
const change = this.applyTypeahead(this.state.value.change(), item);
this.onChange(change);
this.onChange(change, true);
};
updateMenu = () => {
@ -459,6 +468,14 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
);
};
handlePaste = (event: ClipboardEvent, change: Editor) => {
const pastedValue = event.clipboardData.getData('Text');
const newValue = change.value.change().insertText(pastedValue);
this.onChange(newValue);
return true;
};
render() {
const { disabled } = this.props;
const wrapperClassName = classnames('slate-query-field__wrapper', {
@ -475,6 +492,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
onKeyDown={this.onKeyDown}
onChange={this.onChange}
onFocus={this.handleFocus}
onPaste={this.handlePaste}
placeholder={this.props.placeholder}
plugins={this.plugins}
spellCheck={false}

View File

@ -9,20 +9,14 @@ import QueryEditor from './QueryEditor';
import QueryTransactionStatus from './QueryTransactionStatus';
// Actions
import {
addQueryRow,
changeQuery,
highlightLogsExpression,
modifyQueries,
removeQueryRow,
runQueries,
} from './state/actions';
import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions';
// Types
import { StoreState } from 'app/types';
import { RawTimeRange, DataQuery, QueryHint } from '@grafana/ui';
import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui';
import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter';
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@ -37,16 +31,16 @@ interface QueryRowProps {
changeQuery: typeof changeQuery;
className?: string;
exploreId: ExploreId;
datasourceInstance: any;
highlightLogsExpression: typeof highlightLogsExpression;
datasourceInstance: ExploreDataSourceApi;
highlightLogsExpressionAction: typeof highlightLogsExpressionAction;
history: HistoryItem[];
index: number;
initialQuery: DataQuery;
query: DataQuery;
modifyQueries: typeof modifyQueries;
queryTransactions: QueryTransaction[];
exploreEvents: Emitter;
range: RawTimeRange;
removeQueryRow: typeof removeQueryRow;
removeQueryRowAction: typeof removeQueryRowAction;
runQueries: typeof runQueries;
}
@ -78,29 +72,30 @@ export class QueryRow extends PureComponent<QueryRowProps> {
this.onChangeQuery(null, true);
};
onClickHintFix = action => {
onClickHintFix = (action: QueryFixAction) => {
const { datasourceInstance, exploreId, index } = this.props;
if (datasourceInstance && datasourceInstance.modifyQuery) {
const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
const modifier = (queries: DataQuery, action: QueryFixAction) => datasourceInstance.modifyQuery(queries, action);
this.props.modifyQueries(exploreId, action, index, modifier);
}
};
onClickRemoveButton = () => {
const { exploreId, index } = this.props;
this.props.removeQueryRow(exploreId, index);
this.props.removeQueryRowAction({ exploreId, index });
};
updateLogsHighlights = _.debounce((value: DataQuery) => {
const { datasourceInstance } = this.props;
if (datasourceInstance.getHighlighterExpression) {
const { exploreId } = this.props;
const expressions = [datasourceInstance.getHighlighterExpression(value)];
this.props.highlightLogsExpression(this.props.exploreId, expressions);
this.props.highlightLogsExpressionAction({ exploreId, expressions });
}
}, 500);
render() {
const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
const { datasourceInstance, history, index, query, queryTransactions, exploreEvents, range } = this.props;
const transactions = queryTransactions.filter(t => t.rowIndex === index);
const transactionWithError = transactions.find(t => t.error !== undefined);
const hint = getFirstHintFromTransactions(transactions);
@ -115,12 +110,12 @@ export class QueryRow extends PureComponent<QueryRowProps> {
{QueryField ? (
<QueryField
datasource={datasourceInstance}
query={query}
error={queryError}
hint={hint}
initialQuery={initialQuery}
history={history}
onClickHintFix={this.onClickHintFix}
onPressEnter={this.onExecuteQuery}
onExecuteQuery={this.onExecuteQuery}
onExecuteHint={this.onClickHintFix}
onQueryChange={this.onChangeQuery}
/>
) : (
@ -129,7 +124,7 @@ export class QueryRow extends PureComponent<QueryRowProps> {
error={queryError}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.onExecuteQuery}
initialQuery={initialQuery}
initialQuery={query}
exploreEvents={exploreEvents}
range={range}
/>
@ -160,17 +155,17 @@ export class QueryRow extends PureComponent<QueryRowProps> {
function mapStateToProps(state: StoreState, { exploreId, index }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
const initialQuery = initialQueries[index];
return { datasourceInstance, history, initialQuery, queryTransactions, range };
const { datasourceInstance, history, queries, queryTransactions, range } = item;
const query = queries[index];
return { datasourceInstance, history, query, queryTransactions, range };
}
const mapDispatchToProps = {
addQueryRow,
changeQuery,
highlightLogsExpression,
highlightLogsExpressionAction,
modifyQueries,
removeQueryRow,
removeQueryRowAction,
runQueries,
};

View File

@ -6,25 +6,23 @@ import QueryRow from './QueryRow';
// Types
import { Emitter } from 'app/core/utils/emitter';
import { DataQuery } from '@grafana/ui/src/types';
import { ExploreId } from 'app/types/explore';
interface QueryRowsProps {
className?: string;
exploreEvents: Emitter;
exploreId: ExploreId;
initialQueries: DataQuery[];
queryKeys: string[];
}
export default class QueryRows extends PureComponent<QueryRowsProps> {
render() {
const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
return (
<div className={className}>
{initialQueries.map((query, index) => (
// TODO instead of relying on initialQueries, move to react key list in redux
<QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
))}
{queryKeys.map((key, index) => {
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
})}
</div>
);
}

View File

@ -7,16 +7,16 @@ import { StoreState } from 'app/types';
import { ExploreId, ExploreUrlState } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
import { initializeExploreSplit, resetExplore } from './state/actions';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
import { CustomScrollbar } from '@grafana/ui';
import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes';
interface WrapperProps {
initializeExploreSplit: typeof initializeExploreSplit;
initializeExploreSplitAction: typeof initializeExploreSplitAction;
split: boolean;
updateLocation: typeof updateLocation;
resetExplore: typeof resetExplore;
resetExploreAction: typeof resetExploreAction;
urlStates: { [key: string]: string };
}
@ -39,12 +39,12 @@ export class Wrapper extends Component<WrapperProps> {
componentDidMount() {
if (this.initialSplit) {
this.props.initializeExploreSplit();
this.props.initializeExploreSplitAction();
}
}
componentWillUnmount() {
this.props.resetExplore();
this.props.resetExploreAction();
}
render() {
@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => {
};
const mapDispatchToProps = {
initializeExploreSplit,
initializeExploreSplitAction,
updateLocation,
resetExplore,
resetExploreAction,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));

View File

@ -1,6 +1,13 @@
// Types
import { Emitter } from 'app/core/core';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi } from '@grafana/ui/src/types';
import {
RawTimeRange,
TimeRange,
DataQuery,
DataSourceSelectItem,
DataSourceApi,
QueryFixAction,
} from '@grafana/ui/src/types';
import {
ExploreId,
ExploreItemState,
@ -10,234 +17,26 @@ import {
QueryTransaction,
ExploreUIState,
} from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
/** Higher order actions
*
*/
export enum ActionTypes {
AddQueryRow = 'explore/ADD_QUERY_ROW',
ChangeDatasource = 'explore/CHANGE_DATASOURCE',
ChangeQuery = 'explore/CHANGE_QUERY',
ChangeSize = 'explore/CHANGE_SIZE',
ChangeTime = 'explore/CHANGE_TIME',
ClearQueries = 'explore/CLEAR_QUERIES',
HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
InitializeExplore = 'explore/INITIALIZE_EXPLORE',
InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
ModifyQueries = 'explore/MODIFY_QUERIES',
QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
RunQueries = 'explore/RUN_QUERIES',
RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
ScanRange = 'explore/SCAN_RANGE',
ScanStart = 'explore/SCAN_START',
ScanStop = 'explore/SCAN_STOP',
SetQueries = 'explore/SET_QUERIES',
SplitClose = 'explore/SPLIT_CLOSE',
SplitOpen = 'explore/SPLIT_OPEN',
StateSave = 'explore/STATE_SAVE',
ToggleGraph = 'explore/TOGGLE_GRAPH',
ToggleLogs = 'explore/TOGGLE_LOGS',
ToggleTable = 'explore/TOGGLE_TABLE',
UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE',
ResetExplore = 'explore/RESET_EXPLORE',
QueriesImported = 'explore/QueriesImported',
}
export interface AddQueryRowAction {
type: ActionTypes.AddQueryRow;
payload: {
exploreId: ExploreId;
index: number;
query: DataQuery;
};
}
export interface ChangeQueryAction {
type: ActionTypes.ChangeQuery;
payload: {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
};
}
export interface ChangeSizeAction {
type: ActionTypes.ChangeSize;
payload: {
exploreId: ExploreId;
width: number;
height: number;
};
}
export interface ChangeTimeAction {
type: ActionTypes.ChangeTime;
payload: {
exploreId: ExploreId;
range: TimeRange;
};
}
export interface ClearQueriesAction {
type: ActionTypes.ClearQueries;
payload: {
exploreId: ExploreId;
};
}
export interface HighlightLogsExpressionAction {
type: ActionTypes.HighlightLogsExpression;
payload: {
exploreId: ExploreId;
expressions: string[];
};
}
export interface InitializeExploreAction {
type: ActionTypes.InitializeExplore;
payload: {
exploreId: ExploreId;
containerWidth: number;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
ui: ExploreUIState;
};
}
export interface InitializeExploreSplitAction {
type: ActionTypes.InitializeExploreSplit;
}
export interface LoadDatasourceFailureAction {
type: ActionTypes.LoadDatasourceFailure;
payload: {
exploreId: ExploreId;
error: string;
};
}
export interface LoadDatasourcePendingAction {
type: ActionTypes.LoadDatasourcePending;
payload: {
exploreId: ExploreId;
requestedDatasourceName: string;
};
}
export interface LoadDatasourceMissingAction {
type: ActionTypes.LoadDatasourceMissing;
payload: {
exploreId: ExploreId;
};
}
export interface LoadDatasourceSuccessAction {
type: ActionTypes.LoadDatasourceSuccess;
payload: {
exploreId: ExploreId;
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
logsHighlighterExpressions?: any[];
showingStartPage: boolean;
supportsGraph: boolean;
supportsLogs: boolean;
supportsTable: boolean;
};
}
export interface ModifyQueriesAction {
type: ActionTypes.ModifyQueries;
payload: {
exploreId: ExploreId;
modification: any;
index: number;
modifier: (queries: DataQuery[], modification: any) => DataQuery[];
};
}
export interface QueryTransactionFailureAction {
type: ActionTypes.QueryTransactionFailure;
payload: {
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
};
}
export interface QueryTransactionStartAction {
type: ActionTypes.QueryTransactionStart;
payload: {
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
};
}
export interface QueryTransactionSuccessAction {
type: ActionTypes.QueryTransactionSuccess;
payload: {
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
};
}
export interface RemoveQueryRowAction {
type: ActionTypes.RemoveQueryRow;
payload: {
exploreId: ExploreId;
index: number;
};
}
export interface RunQueriesEmptyAction {
type: ActionTypes.RunQueriesEmpty;
payload: {
exploreId: ExploreId;
};
}
export interface ScanStartAction {
type: ActionTypes.ScanStart;
payload: {
exploreId: ExploreId;
scanner: RangeScanner;
};
}
export interface ScanRangeAction {
type: ActionTypes.ScanRange;
payload: {
exploreId: ExploreId;
range: RawTimeRange;
};
}
export interface ScanStopAction {
type: ActionTypes.ScanStop;
payload: {
exploreId: ExploreId;
};
}
export interface SetQueriesAction {
type: ActionTypes.SetQueries;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
payload: {};
}
export interface SplitCloseAction {
type: ActionTypes.SplitClose;
payload: {};
}
export interface SplitOpenAction {
@ -247,80 +46,385 @@ export interface SplitOpenAction {
};
}
export interface StateSaveAction {
type: ActionTypes.StateSave;
}
export interface ToggleTableAction {
type: ActionTypes.ToggleTable;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleGraphAction {
type: ActionTypes.ToggleGraph;
payload: {
exploreId: ExploreId;
};
}
export interface ToggleLogsAction {
type: ActionTypes.ToggleLogs;
payload: {
exploreId: ExploreId;
};
}
export interface UpdateDatasourceInstanceAction {
type: ActionTypes.UpdateDatasourceInstance;
payload: {
exploreId: ExploreId;
datasourceInstance: DataSourceApi;
};
}
export interface ResetExploreAction {
type: ActionTypes.ResetExplore;
payload: {};
}
export interface QueriesImported {
type: ActionTypes.QueriesImported;
payload: {
exploreId: ExploreId;
queries: DataQuery[];
};
/** Lower order actions
*
*/
export interface AddQueryRowPayload {
exploreId: ExploreId;
index: number;
query: DataQuery;
}
export type Action =
| AddQueryRowAction
| ChangeQueryAction
| ChangeSizeAction
| ChangeTimeAction
| ClearQueriesAction
| HighlightLogsExpressionAction
| InitializeExploreAction
export interface ChangeQueryPayload {
exploreId: ExploreId;
query: DataQuery;
index: number;
override: boolean;
}
export interface ChangeSizePayload {
exploreId: ExploreId;
width: number;
height: number;
}
export interface ChangeTimePayload {
exploreId: ExploreId;
range: TimeRange;
}
export interface ClearQueriesPayload {
exploreId: ExploreId;
}
export interface HighlightLogsExpressionPayload {
exploreId: ExploreId;
expressions: string[];
}
export interface InitializeExplorePayload {
exploreId: ExploreId;
containerWidth: number;
eventBridge: Emitter;
exploreDatasources: DataSourceSelectItem[];
queries: DataQuery[];
range: RawTimeRange;
ui: ExploreUIState;
}
export interface LoadDatasourceFailurePayload {
exploreId: ExploreId;
error: string;
}
export interface LoadDatasourceMissingPayload {
exploreId: ExploreId;
}
export interface LoadDatasourcePendingPayload {
exploreId: ExploreId;
requestedDatasourceName: string;
}
export interface LoadDatasourceSuccessPayload {
exploreId: ExploreId;
StartPage?: any;
datasourceInstance: any;
history: HistoryItem[];
logsHighlighterExpressions?: any[];
showingStartPage: boolean;
supportsGraph: boolean;
supportsLogs: boolean;
supportsTable: boolean;
}
export interface ModifyQueriesPayload {
exploreId: ExploreId;
modification: QueryFixAction;
index: number;
modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery;
}
export interface QueryTransactionFailurePayload {
exploreId: ExploreId;
queryTransactions: QueryTransaction[];
}
export interface QueryTransactionStartPayload {
exploreId: ExploreId;
resultType: ResultType;
rowIndex: number;
transaction: QueryTransaction;
}
export interface QueryTransactionSuccessPayload {
exploreId: ExploreId;
history: HistoryItem[];
queryTransactions: QueryTransaction[];
}
export interface RemoveQueryRowPayload {
exploreId: ExploreId;
index: number;
}
export interface RunQueriesEmptyPayload {
exploreId: ExploreId;
}
export interface ScanStartPayload {
exploreId: ExploreId;
scanner: RangeScanner;
}
export interface ScanRangePayload {
exploreId: ExploreId;
range: RawTimeRange;
}
export interface ScanStopPayload {
exploreId: ExploreId;
}
export interface SetQueriesPayload {
exploreId: ExploreId;
queries: DataQuery[];
}
export interface SplitOpenPayload {
itemState: ExploreItemState;
}
export interface ToggleTablePayload {
exploreId: ExploreId;
}
export interface ToggleGraphPayload {
exploreId: ExploreId;
}
export interface ToggleLogsPayload {
exploreId: ExploreId;
}
export interface UpdateDatasourceInstancePayload {
exploreId: ExploreId;
datasourceInstance: DataSourceApi;
}
export interface QueriesImportedPayload {
exploreId: ExploreId;
queries: DataQuery[];
}
/**
* Adds a query row after the row with the given index.
*/
export const addQueryRowAction = actionCreatorFactory<AddQueryRowPayload>('explore/ADD_QUERY_ROW').create();
/**
* Loads a new datasource identified by the given name.
*/
export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create();
/**
* Query change handler for the query row with the given index.
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
*/
export const changeQueryAction = actionCreatorFactory<ChangeQueryPayload>('explore/CHANGE_QUERY').create();
/**
* Keep track of the Explore container size, in particular the width.
* The width will be used to calculate graph intervals (number of datapoints).
*/
export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore/CHANGE_SIZE').create();
/**
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
*/
export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
/**
* Clear all queries and results.
*/
export const clearQueriesAction = actionCreatorFactory<ClearQueriesPayload>('explore/CLEAR_QUERIES').create();
/**
* Highlight expressions in the log results
*/
export const highlightLogsExpressionAction = actionCreatorFactory<HighlightLogsExpressionPayload>(
'explore/HIGHLIGHT_LOGS_EXPRESSION'
).create();
/**
* Initialize Explore state with state from the URL and the React component.
* Call this only on components for with the Explore state has not been initialized.
*/
export const initializeExploreAction = actionCreatorFactory<InitializeExplorePayload>(
'explore/INITIALIZE_EXPLORE'
).create();
/**
* Initialize the wrapper split state
*/
export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create();
/**
* Display an error that happened during the selection of a datasource
*/
export const loadDatasourceFailureAction = actionCreatorFactory<LoadDatasourceFailurePayload>(
'explore/LOAD_DATASOURCE_FAILURE'
).create();
/**
* Display an error when no datasources have been configured
*/
export const loadDatasourceMissingAction = actionCreatorFactory<LoadDatasourceMissingPayload>(
'explore/LOAD_DATASOURCE_MISSING'
).create();
/**
* Start the async process of loading a datasource to display a loading indicator
*/
export const loadDatasourcePendingAction = actionCreatorFactory<LoadDatasourcePendingPayload>(
'explore/LOAD_DATASOURCE_PENDING'
).create();
/**
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
* e.g., Prometheus -> Loki queries.
*/
export const loadDatasourceSuccessAction = actionCreatorFactory<LoadDatasourceSuccessPayload>(
'explore/LOAD_DATASOURCE_SUCCESS'
).create();
/**
* Action to modify a query given a datasource-specific modifier action.
* @param exploreId Explore area
* @param modification Action object with a type, e.g., ADD_FILTER
* @param index Optional query row index. If omitted, the modification is applied to all query rows.
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
*/
export const modifyQueriesAction = actionCreatorFactory<ModifyQueriesPayload>('explore/MODIFY_QUERIES').create();
/**
* Mark a query transaction as failed with an error extracted from the query response.
* The transaction will be marked as `done`.
*/
export const queryTransactionFailureAction = actionCreatorFactory<QueryTransactionFailurePayload>(
'explore/QUERY_TRANSACTION_FAILURE'
).create();
/**
* Start a query transaction for the given result type.
* @param exploreId Explore area
* @param transaction Query options and `done` status.
* @param resultType Associate the transaction with a result viewer, e.g., Graph
* @param rowIndex Index is used to associate latency for this transaction with a query row
*/
export const queryTransactionStartAction = actionCreatorFactory<QueryTransactionStartPayload>(
'explore/QUERY_TRANSACTION_START'
).create();
/**
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
* If the transaction was started by a scanner, it keeps on scanning for more results.
* Side-effect: the query is stored in localStorage.
* @param exploreId Explore area
* @param transactionId ID
* @param result Response from `datasourceInstance.query()`
* @param latency Duration between request and response
* @param queries Queries from all query rows
* @param datasourceId Origin datasource instance, used to discard results if current datasource is different
*/
export const queryTransactionSuccessAction = actionCreatorFactory<QueryTransactionSuccessPayload>(
'explore/QUERY_TRANSACTION_SUCCESS'
).create();
/**
* Remove query row of the given index, as well as associated query results.
*/
export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create();
export const runQueriesEmptyAction = actionCreatorFactory<RunQueriesEmptyPayload>('explore/RUN_QUERIES_EMPTY').create();
/**
* Start a scan for more results using the given scanner.
* @param exploreId Explore area
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
*/
export const scanStartAction = actionCreatorFactory<ScanStartPayload>('explore/SCAN_START').create();
export const scanRangeAction = actionCreatorFactory<ScanRangePayload>('explore/SCAN_RANGE').create();
/**
* Stop any scanning for more results.
*/
export const scanStopAction = actionCreatorFactory<ScanStopPayload>('explore/SCAN_STOP').create();
/**
* Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run.
*/
export const setQueriesAction = actionCreatorFactory<SetQueriesPayload>('explore/SET_QUERIES').create();
/**
* Close the split view and save URL state.
*/
export const splitCloseAction = noPayloadActionCreatorFactory('explore/SPLIT_CLOSE').create();
/**
* Open the split view and copy the left state to be the right state.
* The right state is automatically initialized.
* The copy keeps all query modifications but wipes the query results.
*/
export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/SPLIT_OPEN').create();
export const stateSaveAction = noPayloadActionCreatorFactory('explore/STATE_SAVE').create();
/**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export const toggleTableAction = actionCreatorFactory<ToggleTablePayload>('explore/TOGGLE_TABLE').create();
/**
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
*/
export const toggleGraphAction = actionCreatorFactory<ToggleGraphPayload>('explore/TOGGLE_GRAPH').create();
/**
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
*/
export const toggleLogsAction = actionCreatorFactory<ToggleLogsPayload>('explore/TOGGLE_LOGS').create();
/**
* Updates datasource instance before datasouce loading has started
*/
export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasourceInstancePayload>(
'explore/UPDATE_DATASOURCE_INSTANCE'
).create();
/**
* Resets state for explore.
*/
export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create();
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
export type HigherOrderAction =
| InitializeExploreSplitAction
| LoadDatasourceFailureAction
| LoadDatasourceMissingAction
| LoadDatasourcePendingAction
| LoadDatasourceSuccessAction
| ModifyQueriesAction
| QueryTransactionFailureAction
| QueryTransactionStartAction
| QueryTransactionSuccessAction
| RemoveQueryRowAction
| RunQueriesEmptyAction
| ScanRangeAction
| ScanStartAction
| ScanStopAction
| SetQueriesAction
| SplitCloseAction
| SplitOpenAction
| ToggleGraphAction
| ToggleLogsAction
| ToggleTableAction
| UpdateDatasourceInstanceAction
| ResetExploreAction
| QueriesImported;
| ActionOf<any>;
export type Action =
| ActionOf<AddQueryRowPayload>
| ActionOf<ChangeQueryPayload>
| ActionOf<ChangeSizePayload>
| ActionOf<ChangeTimePayload>
| ActionOf<ClearQueriesPayload>
| ActionOf<HighlightLogsExpressionPayload>
| ActionOf<InitializeExplorePayload>
| ActionOf<LoadDatasourceFailurePayload>
| ActionOf<LoadDatasourceMissingPayload>
| ActionOf<LoadDatasourcePendingPayload>
| ActionOf<LoadDatasourceSuccessPayload>
| ActionOf<ModifyQueriesPayload>
| ActionOf<QueryTransactionFailurePayload>
| ActionOf<QueryTransactionStartPayload>
| ActionOf<QueryTransactionSuccessPayload>
| ActionOf<RemoveQueryRowPayload>
| ActionOf<RunQueriesEmptyPayload>
| ActionOf<ScanStartPayload>
| ActionOf<ScanRangePayload>
| ActionOf<SetQueriesPayload>
| ActionOf<SplitOpenPayload>
| ActionOf<ToggleTablePayload>
| ActionOf<ToggleGraphPayload>
| ActionOf<ToggleLogsPayload>
| ActionOf<UpdateDatasourceInstancePayload>
| ActionOf<QueriesImportedPayload>;

View File

@ -30,41 +30,54 @@ import {
DataQuery,
DataSourceSelectItem,
QueryHint,
QueryFixAction,
} from '@grafana/ui/src/types';
import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions, ExploreUIState } from 'app/types/explore';
import {
ExploreId,
ExploreUrlState,
RangeScanner,
ResultType,
QueryOptions,
QueryTransaction,
ExploreUIState,
} from 'app/types/explore';
import {
Action as ThunkableAction,
ActionTypes,
AddQueryRowAction,
ChangeSizeAction,
HighlightLogsExpressionAction,
LoadDatasourceFailureAction,
LoadDatasourceMissingAction,
LoadDatasourcePendingAction,
LoadDatasourceSuccessAction,
QueryTransactionStartAction,
ScanStopAction,
UpdateDatasourceInstanceAction,
QueriesImported,
Action,
updateDatasourceInstanceAction,
changeQueryAction,
changeSizeAction,
ChangeSizePayload,
changeTimeAction,
scanStopAction,
clearQueriesAction,
initializeExploreAction,
loadDatasourceMissingAction,
loadDatasourceFailureAction,
loadDatasourcePendingAction,
queriesImportedAction,
LoadDatasourceSuccessPayload,
loadDatasourceSuccessAction,
modifyQueriesAction,
queryTransactionFailureAction,
queryTransactionStartAction,
queryTransactionSuccessAction,
scanRangeAction,
runQueriesEmptyAction,
scanStartAction,
setQueriesAction,
splitCloseAction,
splitOpenAction,
addQueryRowAction,
AddQueryRowPayload,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
ToggleGraphPayload,
ToggleLogsPayload,
ToggleTablePayload,
} from './actionTypes';
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
/**
* Adds a query row after the row with the given index.
*/
export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
// /**
// * Adds a query row after the row with the given index.
// */
export function addQueryRow(exploreId: ExploreId, index: number): ActionOf<AddQueryRowPayload> {
const query = generateEmptyQuery(index + 1);
return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
return addQueryRowAction({ exploreId, index, query });
}
/**
@ -74,11 +87,11 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
return async (dispatch, getState) => {
const newDataSourceInstance = await getDatasourceSrv().get(datasource);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
const modifiedQueries = getState().explore[exploreId].modifiedQueries;
const queries = getState().explore[exploreId].queries;
await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance));
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance));
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
try {
await dispatch(loadDatasource(exploreId, newDataSourceInstance));
@ -107,7 +120,7 @@ export function changeQuery(
query = { ...generateEmptyQuery(index) };
}
dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
dispatch(changeQueryAction({ exploreId, query, index, override }));
if (override) {
dispatch(runQueries(exploreId));
}
@ -121,8 +134,8 @@ export function changeQuery(
export function changeSize(
exploreId: ExploreId,
{ height, width }: { height: number; width: number }
): ChangeSizeAction {
return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
): ActionOf<ChangeSizePayload> {
return changeSizeAction({ exploreId, height, width });
}
/**
@ -130,7 +143,7 @@ export function changeSize(
*/
export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
dispatch(changeTimeAction({ exploreId, range }));
dispatch(runQueries(exploreId));
};
}
@ -140,19 +153,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
*/
export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
return dispatch => {
dispatch(scanStop(exploreId));
dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
dispatch(scanStopAction({ exploreId }));
dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave());
};
}
/**
* Highlight expressions in the log results
*/
export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
}
/**
* Initialize Explore state with state from the URL and the React component.
* Call this only on components for with the Explore state has not been initialized.
@ -175,19 +181,17 @@ export function initializeExplore(
meta: ds.meta,
}));
dispatch({
type: ActionTypes.InitializeExplore,
payload: {
dispatch(
initializeExploreAction({
exploreId,
containerWidth,
datasourceName,
eventBridge,
exploreDatasources,
queries,
range,
ui,
},
});
})
);
if (exploreDatasources.length >= 1) {
let instance;
@ -204,7 +208,7 @@ export function initializeExplore(
instance = await getDatasourceSrv().get();
}
dispatch(updateDatasourceInstance(exploreId, instance));
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance }));
try {
await dispatch(loadDatasource(exploreId, instance));
@ -214,69 +218,17 @@ export function initializeExplore(
}
dispatch(runQueries(exploreId, true));
} else {
dispatch(loadDatasourceMissing(exploreId));
dispatch(loadDatasourceMissingAction({ exploreId }));
}
};
}
/**
* Initialize the wrapper split state
*/
export function initializeExploreSplit() {
return async dispatch => {
dispatch({ type: ActionTypes.InitializeExploreSplit });
};
}
/**
* Display an error that happened during the selection of a datasource
*/
export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
type: ActionTypes.LoadDatasourceFailure,
payload: {
exploreId,
error,
},
});
/**
* Display an error when no datasources have been configured
*/
export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
type: ActionTypes.LoadDatasourceMissing,
payload: { exploreId },
});
/**
* Start the async process of loading a datasource to display a loading indicator
*/
export const loadDatasourcePending = (
exploreId: ExploreId,
requestedDatasourceName: string
): LoadDatasourcePendingAction => ({
type: ActionTypes.LoadDatasourcePending,
payload: {
exploreId,
requestedDatasourceName,
},
});
export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => {
return {
type: ActionTypes.QueriesImported,
payload: {
exploreId,
queries,
},
};
};
/**
* Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
* run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
* e.g., Prometheus -> Loki queries.
*/
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): LoadDatasourceSuccessAction => {
export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf<LoadDatasourceSuccessPayload> => {
// Capabilities
const supportsGraph = instance.meta.metrics;
const supportsLogs = instance.meta.logs;
@ -289,37 +241,18 @@ export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): Load
// Save last-used datasource
store.set(LAST_USED_DATASOURCE_KEY, instance.name);
return {
type: ActionTypes.LoadDatasourceSuccess,
payload: {
exploreId,
StartPage,
datasourceInstance: instance,
history,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
supportsTable,
},
};
return loadDatasourceSuccessAction({
exploreId,
StartPage,
datasourceInstance: instance,
history,
showingStartPage: Boolean(StartPage),
supportsGraph,
supportsLogs,
supportsTable,
});
};
/**
* Updates datasource instance before datasouce loading has started
*/
export function updateDatasourceInstance(
exploreId: ExploreId,
instance: DataSourceApi
): UpdateDatasourceInstanceAction {
return {
type: ActionTypes.UpdateDatasourceInstance,
payload: {
exploreId,
datasourceInstance: instance,
},
};
}
export function importQueries(
exploreId: ExploreId,
queries: DataQuery[],
@ -341,11 +274,11 @@ export function importQueries(
}
const nextQueries = importedQueries.map((q, i) => ({
...importedQueries[i],
...q,
...generateEmptyQuery(i),
}));
dispatch(queriesImported(exploreId, nextQueries));
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
};
}
@ -357,7 +290,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
const datasourceName = instance.name;
// Keep ID to track selection
dispatch(loadDatasourcePending(exploreId, datasourceName));
dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName }));
let datasourceError = null;
try {
@ -368,7 +301,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
}
if (datasourceError) {
dispatch(loadDatasourceFailure(exploreId, datasourceError));
dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError }));
return Promise.reject(`${datasourceName} loading failed`);
}
@ -400,12 +333,12 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T
*/
export function modifyQueries(
exploreId: ExploreId,
modification: any,
modification: QueryFixAction,
index: number,
modifier: any
): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
dispatch(modifyQueriesAction({ exploreId, modification, index, modifier }));
if (!modification.preventSubmit) {
dispatch(runQueries(exploreId));
}
@ -470,29 +403,10 @@ export function queryTransactionFailure(
return qt;
});
dispatch({
type: ActionTypes.QueryTransactionFailure,
payload: { exploreId, queryTransactions: nextQueryTransactions },
});
dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions }));
};
}
/**
* Start a query transaction for the given result type.
* @param exploreId Explore area
* @param transaction Query options and `done` status.
* @param resultType Associate the transaction with a result viewer, e.g., Graph
* @param rowIndex Index is used to associate latency for this transaction with a query row
*/
export function queryTransactionStart(
exploreId: ExploreId,
transaction: QueryTransaction,
resultType: ResultType,
rowIndex: number
): QueryTransactionStartAction {
return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
}
/**
* Complete a query transaction, mark the transaction as `done` and store query state in URL.
* If the transaction was started by a scanner, it keeps on scanning for more results.
@ -549,14 +463,13 @@ export function queryTransactionSuccess(
// Side-effect: Saving history in localstorage
const nextHistory = updateHistory(history, datasourceId, queries);
dispatch({
type: ActionTypes.QueryTransactionSuccess,
payload: {
dispatch(
queryTransactionSuccessAction({
exploreId,
history: nextHistory,
queryTransactions: nextQueryTransactions,
},
});
})
);
// Keep scanning for results if this was the last scanning transaction
if (scanning) {
@ -564,26 +477,16 @@ export function queryTransactionSuccess(
const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
if (!other) {
const range = scanner();
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
dispatch(scanRangeAction({ exploreId, range }));
}
} else {
// We can stop scanning if we have a result
dispatch(scanStop(exploreId));
dispatch(scanStopAction({ exploreId }));
}
}
};
}
/**
* Remove query row of the given index, as well as associated query results.
*/
export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
dispatch(runQueries(exploreId));
};
}
/**
* Main action to run queries and dispatches sub-actions based on which result viewers are active
*/
@ -591,7 +494,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
return (dispatch, getState) => {
const {
datasourceInstance,
modifiedQueries,
queries,
showingLogs,
showingGraph,
showingTable,
@ -600,8 +503,8 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) {
supportsTable,
} = getState().explore[exploreId];
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
if (!hasNonEmptyQuery(queries)) {
dispatch(runQueriesEmptyAction({ exploreId }));
dispatch(stateSave()); // Remember to saves to state and update location
return;
}
@ -662,14 +565,7 @@ function runQueriesForType(
resultGetter?: any
) {
return async (dispatch, getState) => {
const {
datasourceInstance,
eventBridge,
modifiedQueries: queries,
queryIntervals,
range,
scanning,
} = getState().explore[exploreId];
const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
const datasourceId = datasourceInstance.meta.id;
// Run all queries concurrently
@ -683,7 +579,7 @@ function runQueriesForType(
queryIntervals,
scanning
);
dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction }));
try {
const now = Date.now();
const res = await datasourceInstance.query(transaction.options);
@ -707,21 +603,14 @@ function runQueriesForType(
export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
return dispatch => {
// Register the scanner
dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
dispatch(scanStartAction({ exploreId, scanner }));
// Scanning must trigger query run, and return the new range
const range = scanner();
// Set the new range to be displayed
dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
dispatch(scanRangeAction({ exploreId, range }));
};
}
/**
* Stop any scanning for more results.
*/
export function scanStop(exploreId: ExploreId): ScanStopAction {
return { type: ActionTypes.ScanStop, payload: { exploreId } };
}
/**
* Reset queries to the given queries. Any modifications will be discarded.
* Use this action for clicks on query examples. Triggers a query run.
@ -730,13 +619,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
return dispatch => {
// Inject react keys into query objects
const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
dispatch({
type: ActionTypes.SetQueries,
payload: {
exploreId,
queries,
},
});
dispatch(setQueriesAction({ exploreId, queries }));
dispatch(runQueries(exploreId));
};
}
@ -746,7 +629,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk
*/
export function splitClose(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.SplitClose });
dispatch(splitCloseAction());
dispatch(stateSave());
};
}
@ -763,9 +646,9 @@ export function splitOpen(): ThunkResult<void> {
const itemState = {
...leftState,
queryTransactions: [],
initialQueries: leftState.modifiedQueries.slice(),
queries: leftState.queries.slice(),
};
dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
dispatch(splitOpenAction({ itemState }));
dispatch(stateSave());
};
}
@ -780,7 +663,7 @@ export function stateSave() {
const urlStates: { [index: string]: string } = {};
const leftUrlState: ExploreUrlState = {
datasource: left.datasourceInstance.name,
queries: left.modifiedQueries.map(clearQueryKeys),
queries: left.queries.map(clearQueryKeys),
range: left.range,
ui: {
showingGraph: left.showingGraph,
@ -792,13 +675,9 @@ export function stateSave() {
if (split) {
const rightUrlState: ExploreUrlState = {
datasource: right.datasourceInstance.name,
queries: right.modifiedQueries.map(clearQueryKeys),
queries: right.queries.map(clearQueryKeys),
range: right.range,
ui: {
showingGraph: right.showingGraph,
showingLogs: right.showingLogs,
showingTable: right.showingTable,
},
ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable },
};
urlStates.right = serializeStateToUrlParam(rightUrlState, true);
@ -812,22 +691,25 @@ export function stateSave() {
* Creates action to collapse graph/logs/table panel. When panel is collapsed,
* queries won't be run
*/
const togglePanelActionCreator = (type: ActionTypes.ToggleGraph | ActionTypes.ToggleTable | ActionTypes.ToggleLogs) => (
exploreId: ExploreId
) => {
const togglePanelActionCreator = (
actionCreator:
| ActionCreator<ToggleGraphPayload>
| ActionCreator<ToggleLogsPayload>
| ActionCreator<ToggleTablePayload>
) => (exploreId: ExploreId) => {
return (dispatch, getState) => {
let shouldRunQueries;
dispatch({ type, payload: { exploreId } });
dispatch(actionCreator({ exploreId }));
dispatch(stateSave());
switch (type) {
case ActionTypes.ToggleGraph:
switch (actionCreator.type) {
case toggleGraphAction.type:
shouldRunQueries = getState().explore[exploreId].showingGraph;
break;
case ActionTypes.ToggleLogs:
case toggleLogsAction.type:
shouldRunQueries = getState().explore[exploreId].showingLogs;
break;
case ActionTypes.ToggleTable:
case toggleTableAction.type:
shouldRunQueries = getState().explore[exploreId].showingTable;
break;
}
@ -841,23 +723,14 @@ const togglePanelActionCreator = (type: ActionTypes.ToggleGraph | ActionTypes.To
/**
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
*/
export const toggleGraph = togglePanelActionCreator(ActionTypes.ToggleGraph);
export const toggleGraph = togglePanelActionCreator(toggleGraphAction);
/**
* Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
*/
export const toggleLogs = togglePanelActionCreator(ActionTypes.ToggleLogs);
export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
/**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export const toggleTable = togglePanelActionCreator(ActionTypes.ToggleTable);
/**
* Resets state for explore.
*/
export function resetExplore(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ResetExplore, payload: {} });
};
}
export const toggleTable = togglePanelActionCreator(toggleTableAction);

View File

@ -1,42 +1,47 @@
import { Action, ActionTypes } from './actionTypes';
import { itemReducer, makeExploreItemState } from './reducers';
import { ExploreId } from 'app/types/explore';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { reducerTester } from 'test/core/redux/reducerTester';
import { scanStartAction, scanStopAction } from './actionTypes';
import { Reducer } from 'redux';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
describe('Explore item reducer', () => {
describe('scanning', () => {
test('should start scanning', () => {
let state = makeExploreItemState();
const action: Action = {
type: ActionTypes.ScanStart,
payload: {
exploreId: ExploreId.left,
scanner: jest.fn(),
},
const scanner = jest.fn();
const initalState = {
...makeExploreItemState(),
scanning: false,
scanner: undefined,
};
state = itemReducer(state, action);
expect(state.scanning).toBeTruthy();
expect(state.scanner).toBe(action.payload.scanner);
reducerTester()
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner }))
.thenStateShouldEqual({
...makeExploreItemState(),
scanning: true,
scanner,
});
});
test('should stop scanning', () => {
let state = makeExploreItemState();
const start: Action = {
type: ActionTypes.ScanStart,
payload: {
exploreId: ExploreId.left,
scanner: jest.fn(),
},
const scanner = jest.fn();
const initalState = {
...makeExploreItemState(),
scanning: true,
scanner,
scanRange: {},
};
state = itemReducer(state, start);
expect(state.scanning).toBeTruthy();
const action: Action = {
type: ActionTypes.ScanStop,
payload: {
exploreId: ExploreId.left,
},
};
state = itemReducer(state, action);
expect(state.scanning).toBeFalsy();
expect(state.scanner).toBeUndefined();
reducerTester()
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
.thenStateShouldEqual({
...makeExploreItemState(),
scanning: false,
scanner: undefined,
scanRange: undefined,
});
});
});
});

View File

@ -3,11 +3,41 @@ import {
generateEmptyQuery,
getIntervals,
ensureQueries,
getQueryKeys,
} from 'app/core/utils/explore';
import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
import { DataQuery } from '@grafana/ui/src/types';
import { Action, ActionTypes } from './actionTypes';
import { HigherOrderAction, ActionTypes } from './actionTypes';
import { reducerFactory } from 'app/core/redux';
import {
addQueryRowAction,
changeQueryAction,
changeSizeAction,
changeTimeAction,
clearQueriesAction,
highlightLogsExpressionAction,
initializeExploreAction,
updateDatasourceInstanceAction,
loadDatasourceFailureAction,
loadDatasourceMissingAction,
loadDatasourcePendingAction,
loadDatasourceSuccessAction,
modifyQueriesAction,
queryTransactionFailureAction,
queryTransactionStartAction,
queryTransactionSuccessAction,
removeQueryRowAction,
runQueriesEmptyAction,
scanRangeAction,
scanStartAction,
scanStopAction,
setQueriesAction,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
queriesImportedAction,
} from './actionTypes';
export const DEFAULT_RANGE = {
from: 'now-6h',
@ -30,9 +60,8 @@ export const makeExploreItemState = (): ExploreItemState => ({
datasourceMissing: false,
exploreDatasources: [],
history: [],
initialQueries: [],
queries: [],
initialized: false,
modifiedQueries: [],
queryTransactions: [],
queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
range: DEFAULT_RANGE,
@ -44,6 +73,7 @@ export const makeExploreItemState = (): ExploreItemState => ({
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
queryKeys: [],
});
/**
@ -58,21 +88,15 @@ export const initialExploreState: ExploreState = {
/**
* Reducer for an Explore area, to be used by the global Explore reducer.
*/
export const itemReducer = (state, action: Action): ExploreItemState => {
switch (action.type) {
case ActionTypes.AddQueryRow: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemState)
.addMapper({
filter: addQueryRowAction,
mapper: (state, action): ExploreItemState => {
const { queries, queryTransactions } = state;
const { index, query } = action.payload;
// Add new query row after given index, keep modifications of existing rows
const nextModifiedQueries = [
...modifiedQueries.slice(0, index + 1),
{ ...query },
...initialQueries.slice(index + 1),
];
// Add to initialQueries, which will cause a new row to be rendered
const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
// Add to queries, which will cause a new row to be rendered
const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)];
// Ongoing transactions need to update their row indices
const nextQueryTransactions = queryTransactions.map(qt => {
@ -87,48 +111,38 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
return {
...state,
initialQueries: nextQueries,
queries: nextQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: nextModifiedQueries,
queryTransactions: nextQueryTransactions,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
};
}
case ActionTypes.ChangeQuery: {
const { initialQueries, queryTransactions } = state;
let { modifiedQueries } = state;
const { query, index, override } = action.payload;
// Fast path: only change modifiedQueries to not trigger an update
modifiedQueries[index] = query;
if (!override) {
return {
...state,
modifiedQueries,
};
}
},
})
.addMapper({
filter: changeQueryAction,
mapper: (state, action): ExploreItemState => {
const { queries, queryTransactions } = state;
const { query, index } = action.payload;
// Override path: queries are completely reset
const nextQuery: DataQuery = {
...query,
...generateEmptyQuery(index),
};
const nextQueries = [...initialQueries];
const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
modifiedQueries = [...nextQueries];
// Discard ongoing transaction related to row query
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
return {
...state,
initialQueries: nextQueries,
modifiedQueries: nextQueries.slice(),
queries: nextQueries,
queryTransactions: nextQueryTransactions,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
};
}
case ActionTypes.ChangeSize: {
},
})
.addMapper({
filter: changeSizeAction,
mapper: (state, action): ExploreItemState => {
const { range, datasourceInstance } = state;
let interval = '1s';
if (datasourceInstance && datasourceInstance.interval) {
@ -137,32 +151,37 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
const containerWidth = action.payload.width;
const queryIntervals = getIntervals(range, interval, containerWidth);
return { ...state, containerWidth, queryIntervals };
}
case ActionTypes.ChangeTime: {
return {
...state,
range: action.payload.range,
};
}
case ActionTypes.ClearQueries: {
},
})
.addMapper({
filter: changeTimeAction,
mapper: (state, action): ExploreItemState => {
return { ...state, range: action.payload.range };
},
})
.addMapper({
filter: clearQueriesAction,
mapper: (state): ExploreItemState => {
const queries = ensureQueries();
return {
...state,
initialQueries: queries.slice(),
modifiedQueries: queries.slice(),
queries: queries.slice(),
queryTransactions: [],
showingStartPage: Boolean(state.StartPage),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
};
}
case ActionTypes.HighlightLogsExpression: {
},
})
.addMapper({
filter: highlightLogsExpressionAction,
mapper: (state, action): ExploreItemState => {
const { expressions } = action.payload;
return { ...state, logsHighlighterExpressions: expressions };
}
case ActionTypes.InitializeExplore: {
},
})
.addMapper({
filter: initializeExploreAction,
mapper: (state, action): ExploreItemState => {
const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload;
return {
...state,
@ -170,35 +189,41 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
eventBridge,
exploreDatasources,
range,
initialQueries: queries,
queries,
initialized: true,
modifiedQueries: queries.slice(),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
...ui,
};
}
case ActionTypes.UpdateDatasourceInstance: {
},
})
.addMapper({
filter: updateDatasourceInstanceAction,
mapper: (state, action): ExploreItemState => {
const { datasourceInstance } = action.payload;
return {
...state,
datasourceInstance,
datasourceName: datasourceInstance.name,
};
}
case ActionTypes.LoadDatasourceFailure: {
return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) };
},
})
.addMapper({
filter: loadDatasourceFailureAction,
mapper: (state, action): ExploreItemState => {
return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
}
case ActionTypes.LoadDatasourceMissing: {
},
})
.addMapper({
filter: loadDatasourceMissingAction,
mapper: (state): ExploreItemState => {
return { ...state, datasourceMissing: true, datasourceLoading: false };
}
case ActionTypes.LoadDatasourcePending: {
},
})
.addMapper({
filter: loadDatasourcePendingAction,
mapper: (state, action): ExploreItemState => {
return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName };
}
case ActionTypes.LoadDatasourceSuccess: {
},
})
.addMapper({
filter: loadDatasourceSuccessAction,
mapper: (state, action): ExploreItemState => {
const { containerWidth, range } = state;
const {
StartPage,
@ -227,32 +252,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
logsHighlighterExpressions: undefined,
queryTransactions: [],
};
}
case ActionTypes.ModifyQueries: {
const { initialQueries, modifiedQueries, queryTransactions } = state;
const { modification, index, modifier } = action.payload as any;
},
})
.addMapper({
filter: modifyQueriesAction,
mapper: (state, action): ExploreItemState => {
const { queries, queryTransactions } = state;
const { modification, index, modifier } = action.payload;
let nextQueries: DataQuery[];
let nextQueryTransactions;
if (index === undefined) {
// Modify all queries
nextQueries = initialQueries.map((query, i) => ({
...modifier(modifiedQueries[i], modification),
nextQueries = queries.map((query, i) => ({
...modifier({ ...query }, modification),
...generateEmptyQuery(i),
}));
// Discard all ongoing transactions
nextQueryTransactions = [];
} else {
// Modify query only at index
nextQueries = initialQueries.map((query, i) => {
nextQueries = queries.map((query, i) => {
// Synchronize all queries with local query cache to ensure consistency
// TODO still needed?
return i === index
? {
...modifier(modifiedQueries[i], modification),
...generateEmptyQuery(i),
}
: query;
return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
});
nextQueryTransactions = queryTransactions
// Consume the hint corresponding to the action
@ -267,22 +289,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
}
return {
...state,
initialQueries: nextQueries,
modifiedQueries: nextQueries.slice(),
queries: nextQueries,
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
queryTransactions: nextQueryTransactions,
};
}
case ActionTypes.QueryTransactionFailure: {
},
})
.addMapper({
filter: queryTransactionFailureAction,
mapper: (state, action): ExploreItemState => {
const { queryTransactions } = action.payload;
return {
...state,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionStart: {
return { ...state, queryTransactions, showingStartPage: false };
},
})
.addMapper({
filter: queryTransactionStartAction,
mapper: (state, action): ExploreItemState => {
const { queryTransactions } = state;
const { resultType, rowIndex, transaction } = action.payload;
// Discarding existing transactions of same type
@ -293,14 +315,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
// Append new transaction
const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
return {
...state,
queryTransactions: nextQueryTransactions,
showingStartPage: false,
};
}
case ActionTypes.QueryTransactionSuccess: {
return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false };
},
})
.addMapper({
filter: queryTransactionSuccessAction,
mapper: (state, action): ExploreItemState => {
const { datasourceInstance, queryIntervals } = state;
const { history, queryTransactions } = action.payload;
const results = calculateResultsFromQueryTransactions(
@ -309,30 +329,24 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
queryIntervals.intervalMs
);
return {
...state,
...results,
history,
queryTransactions,
showingStartPage: false,
};
}
case ActionTypes.RemoveQueryRow: {
const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
let { modifiedQueries } = state;
return { ...state, ...results, history, queryTransactions, showingStartPage: false };
},
})
.addMapper({
filter: removeQueryRowAction,
mapper: (state, action): ExploreItemState => {
const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state;
const { index } = action.payload;
modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
if (initialQueries.length <= 1) {
if (queries.length <= 1) {
return state;
}
const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)];
// Discard transactions related to row query
const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key));
const results = calculateResultsFromQueryTransactions(
nextQueryTransactions,
datasourceInstance,
@ -342,26 +356,34 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
return {
...state,
...results,
initialQueries: nextQueries,
queries: nextQueries,
logsHighlighterExpressions: undefined,
modifiedQueries: nextQueries.slice(),
queryTransactions: nextQueryTransactions,
queryKeys: nextQueryKeys,
};
}
case ActionTypes.RunQueriesEmpty: {
},
})
.addMapper({
filter: runQueriesEmptyAction,
mapper: (state): ExploreItemState => {
return { ...state, queryTransactions: [] };
}
case ActionTypes.ScanRange: {
},
})
.addMapper({
filter: scanRangeAction,
mapper: (state, action): ExploreItemState => {
return { ...state, scanRange: action.payload.range };
}
case ActionTypes.ScanStart: {
},
})
.addMapper({
filter: scanStartAction,
mapper: (state, action): ExploreItemState => {
return { ...state, scanning: true, scanner: action.payload.scanner };
}
case ActionTypes.ScanStop: {
},
})
.addMapper({
filter: scanStopAction,
mapper: (state): ExploreItemState => {
const { queryTransactions } = state;
const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
return {
@ -371,14 +393,22 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
scanRange: undefined,
scanner: undefined,
};
}
case ActionTypes.SetQueries: {
},
})
.addMapper({
filter: setQueriesAction,
mapper: (state, action): ExploreItemState => {
const { queries } = action.payload;
return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
}
case ActionTypes.ToggleGraph: {
return {
...state,
queries: queries.slice(),
queryKeys: getQueryKeys(queries, state.datasourceInstance),
};
},
})
.addMapper({
filter: toggleGraphAction,
mapper: (state): ExploreItemState => {
const showingGraph = !state.showingGraph;
let nextQueryTransactions = state.queryTransactions;
if (!showingGraph) {
@ -386,9 +416,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
}
return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
}
case ActionTypes.ToggleLogs: {
},
})
.addMapper({
filter: toggleLogsAction,
mapper: (state): ExploreItemState => {
const showingLogs = !state.showingLogs;
let nextQueryTransactions = state.queryTransactions;
if (!showingLogs) {
@ -396,9 +428,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
}
return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
}
case ActionTypes.ToggleTable: {
},
})
.addMapper({
filter: toggleTableAction,
mapper: (state): ExploreItemState => {
const showingTable = !state.showingTable;
if (showingTable) {
return { ...state, showingTable, queryTransactions: state.queryTransactions };
@ -413,25 +447,26 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
);
return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
}
case ActionTypes.QueriesImported: {
},
})
.addMapper({
filter: queriesImportedAction,
mapper: (state, action): ExploreItemState => {
const { queries } = action.payload;
return {
...state,
initialQueries: action.payload.queries,
modifiedQueries: action.payload.queries.slice(),
queries,
queryKeys: getQueryKeys(queries, state.datasourceInstance),
};
}
}
return state;
};
},
})
.create();
/**
* Global Explore reducer that handles multiple Explore areas (left and right).
* Actions that have an `exploreId` get routed to the ExploreItemReducer.
*/
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => {
switch (action.type) {
case ActionTypes.SplitClose: {
return { ...state, split: false };
@ -454,10 +489,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp
const { exploreId } = action.payload as any;
if (exploreId !== undefined) {
const exploreItemState = state[exploreId];
return {
...state,
[exploreId]: itemReducer(exploreItemState, action),
};
return { ...state, [exploreId]: itemReducer(exploreItemState, action) };
}
}

View File

@ -33,7 +33,7 @@ export class LokiQueryEditor extends PureComponent<Props> {
query: {
...this.state.query,
expr: query.expr,
}
},
});
};
@ -59,14 +59,20 @@ export class LokiQueryEditor extends PureComponent<Props> {
<div>
<LokiQueryField
datasource={datasource}
initialQuery={query}
query={query}
onQueryChange={this.onFieldChange}
onPressEnter={this.onRunQuery}
onExecuteQuery={this.onRunQuery}
history={[]}
/>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label">Format as</div>
<Select isSearchable={false} options={formatOptions} onChange={this.onFormatChanged} value={currentFormat} />
<Select
isSearchable={false}
options={formatOptions}
onChange={this.onFormatChanged}
value={currentFormat}
/>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />

View File

@ -12,12 +12,12 @@ import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explor
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
import LokiDatasource from '../datasource';
// Types
import { LokiQuery } from '../types';
import { TypeaheadOutput } from 'app/types/explore';
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise';
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
const PRISM_SYNTAX = 'promql';
@ -65,15 +65,8 @@ interface CascaderOption {
disabled?: boolean;
}
interface LokiQueryFieldProps {
datasource: LokiDatasource;
error?: string | JSX.Element;
hint?: any;
history?: any[];
initialQuery?: LokiQuery;
onClickHintFix?: (action: any) => void;
onPressEnter?: () => void;
onQueryChange?: (value: LokiQuery, override?: boolean) => void;
interface LokiQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, LokiQuery> {
history: HistoryItem[];
}
interface LokiQueryFieldState {
@ -98,14 +91,14 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
this.plugins = [
BracesPlugin(),
RunnerPlugin({ handler: props.onPressEnter }),
RunnerPlugin({ handler: props.onExecuteQuery }),
PluginPrism({
onlyIn: node => node.type === 'code_block',
getSyntax: node => 'promql',
}),
];
this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })];
this.state = {
logLabelOptions: [],
@ -169,20 +162,21 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
onChangeQuery = (value: string, override?: boolean) => {
// Send text change to parent
const { initialQuery, onQueryChange } = this.props;
const { query, onQueryChange, onExecuteQuery } = this.props;
if (onQueryChange) {
const query = {
...initialQuery,
expr: value,
};
onQueryChange(query, override);
const nextQuery = { ...query, expr: value };
onQueryChange(nextQuery);
if (override && onExecuteQuery) {
onExecuteQuery();
}
}
};
onClickHintFix = () => {
const { hint, onClickHintFix } = this.props;
if (onClickHintFix && hint && hint.fix) {
onClickHintFix(hint.fix.action);
const { hint, onExecuteHint } = this.props;
if (onExecuteHint && hint && hint.fix) {
onExecuteHint(hint.fix.action);
}
};
@ -220,7 +214,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
};
render() {
const { error, hint, initialQuery } = this.props;
const { error, hint, query } = this.props;
const { logLabelOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
@ -240,10 +234,11 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
<QueryField
additionalPlugins={this.plugins}
cleanText={cleanText}
initialQuery={initialQuery.expr}
initialQuery={query.expr}
onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.props.onExecuteQuery}
placeholder="Enter a Loki query"
portalOrigin="loki"
syntaxLoaded={syntaxLoaded}

View File

@ -1,11 +1,8 @@
import React, { PureComponent } from 'react';
import LokiCheatSheet from './LokiCheatSheet';
import { ExploreStartPageProps } from '@grafana/ui';
interface Props {
onClickExample: () => void;
}
export default class LokiStartPage extends PureComponent<Props> {
export default class LokiStartPage extends PureComponent<ExploreStartPageProps> {
render() {
return (
<div className="grafana-info-box grafana-info-box--max-lg">

View File

@ -4,7 +4,7 @@ import Cascader from 'rc-cascader';
import PluginPrism from 'slate-prism';
import Prism from 'prismjs';
import { TypeaheadOutput } from 'app/types/explore';
import { TypeaheadOutput, HistoryItem } from 'app/types/explore';
// dom also includes Element polyfills
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
@ -13,6 +13,7 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
import { PromQuery } from '../types';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { ExploreDataSourceApi, ExploreQueryFieldProps } from '@grafana/ui';
const HISTOGRAM_GROUP = '__histograms__';
const METRIC_MARK = 'metric';
@ -86,15 +87,8 @@ interface CascaderOption {
disabled?: boolean;
}
interface PromQueryFieldProps {
datasource: any;
error?: string | JSX.Element;
initialQuery: PromQuery;
hint?: any;
history?: any[];
onClickHintFix?: (action: any) => void;
onPressEnter?: () => void;
onQueryChange?: (value: PromQuery, override?: boolean) => void;
interface PromQueryFieldProps extends ExploreQueryFieldProps<ExploreDataSourceApi, PromQuery> {
history: HistoryItem[];
}
interface PromQueryFieldState {
@ -116,7 +110,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
this.plugins = [
BracesPlugin(),
RunnerPlugin({ handler: props.onPressEnter }),
RunnerPlugin({ handler: props.onExecuteQuery }),
PluginPrism({
onlyIn: node => node.type === 'code_block',
getSyntax: node => 'promql',
@ -174,20 +168,21 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
onChangeQuery = (value: string, override?: boolean) => {
// Send text change to parent
const { initialQuery, onQueryChange } = this.props;
const { query, onQueryChange, onExecuteQuery } = this.props;
if (onQueryChange) {
const query: PromQuery = {
...initialQuery,
expr: value,
};
onQueryChange(query, override);
const nextQuery: PromQuery = { ...query, expr: value };
onQueryChange(nextQuery);
if (override && onExecuteQuery) {
onExecuteQuery();
}
}
};
onClickHintFix = () => {
const { hint, onClickHintFix } = this.props;
if (onClickHintFix && hint && hint.fix) {
onClickHintFix(hint.fix.action);
const { hint, onExecuteHint } = this.props;
if (onExecuteHint && hint && hint.fix) {
onExecuteHint(hint.fix.action);
}
};
@ -242,7 +237,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
};
render() {
const { error, hint, initialQuery } = this.props;
const { error, hint, query } = this.props;
const { metricsOptions, syntaxLoaded } = this.state;
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
const chooserText = syntaxLoaded ? 'Metrics' : 'Loading metrics...';
@ -261,10 +256,11 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
<QueryField
additionalPlugins={this.plugins}
cleanText={cleanText}
initialQuery={initialQuery.expr}
initialQuery={query.expr}
onTypeahead={this.onTypeahead}
onWillApplySuggestion={willApplySuggestion}
onValueChanged={this.onChangeQuery}
onQueryChange={this.onChangeQuery}
onExecuteQuery={this.props.onExecuteQuery}
placeholder="Enter a PromQL query"
portalOrigin="prometheus"
syntaxLoaded={syntaxLoaded}

View File

@ -1,11 +1,8 @@
import React, { PureComponent } from 'react';
import PromCheatSheet from './PromCheatSheet';
import { ExploreStartPageProps } from '@grafana/ui';
interface Props {
onClickExample: () => void;
}
export default class PromStart extends PureComponent<Props> {
export default class PromStart extends PureComponent<ExploreStartPageProps> {
render() {
return (
<div className="grafana-info-box grafana-info-box--max-lg">

View File

@ -10,21 +10,21 @@ import { Alignments } from './Alignments';
import { AlignmentPeriods } from './AlignmentPeriods';
import { AliasBy } from './AliasBy';
import { Help } from './Help';
import { Target, MetricDescriptor } from '../types';
import { StackdriverQuery, MetricDescriptor } from '../types';
import { getAlignmentPickerData } from '../functions';
import StackdriverDatasource from '../datasource';
import { SelectOptionItem } from '@grafana/ui';
export interface Props {
onQueryChange: (target: Target) => void;
onQueryChange: (target: StackdriverQuery) => void;
onExecuteQuery: () => void;
target: Target;
target: StackdriverQuery;
events: any;
datasource: StackdriverDatasource;
templateSrv: TemplateSrv;
}
interface State extends Target {
interface State extends StackdriverQuery {
alignOptions: SelectOptionItem[];
lastQuery: string;
lastQueryError: string;

View File

@ -2,9 +2,10 @@ import { stackdriverUnitMappings } from './constants';
import appEvents from 'app/core/app_events';
import _ from 'lodash';
import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
import { MetricDescriptor } from './types';
import { StackdriverQuery, MetricDescriptor } from './types';
import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types';
export default class StackdriverDatasource {
export default class StackdriverDatasource implements DataSourceApi<StackdriverQuery> {
id: number;
url: string;
baseUrl: string;
@ -39,9 +40,7 @@ export default class StackdriverDatasource {
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
view: t.view || 'FULL',
filters: (t.filters || []).map(f => {
return this.templateSrv.replace(f, options.scopedVars || {});
}),
filters: this.interpolateFilters(t.filters, options.scopedVars),
aliasBy: this.templateSrv.replace(t.aliasBy, options.scopedVars || {}),
type: 'timeSeriesQuery',
};
@ -63,7 +62,13 @@ export default class StackdriverDatasource {
}
}
async getLabels(metricType, refId) {
interpolateFilters(filters: string[], scopedVars: object) {
return (filters || []).map(f => {
return this.templateSrv.replace(f, scopedVars || {}, 'regex');
});
}
async getLabels(metricType: string, refId: string) {
const response = await this.getTimeSeries({
targets: [
{
@ -103,7 +108,7 @@ export default class StackdriverDatasource {
return unit;
}
async query(options) {
async query(options: DataQueryOptions<StackdriverQuery>) {
const result = [];
const data = await this.getTimeSeries(options);
if (data.results) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
<svg width="100" height="90" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M-1-1h102v92H-1z"/><g><path d="M151.637 29.785c-1.659.621-3.32 1.241-4.783 2.055-1.548-7.686-18.278-8.18-18.117 1.021.148 8.228 18.35 8.414 22.9 16.065 3.456 5.808 1.064 14.28-3.417 17.433-1.805 1.271-4.625 3.234-10.936 3.076-7.568-.19-13.65-5.277-16.065-12.305 1.474-1.151 3.464-1.777 5.468-2.393.087 9.334 18.304 12.687 20.509 3.418 3.661-15.375-24.686-9.097-24.267-25.636.375-14.998 25.388-16.197 28.708-2.734zM207.347 68.413h-5.466v-4.444c-2.872 2.517-5.263 5.222-10.254 5.467-10.316.51-17.038-10.377-10.256-17.773 4.38-4.774 13.169-5.41 20.512-2.05 1.548-10.171-13.626-11.842-16.407-4.44-1.698-.697-3.195-1.592-5.126-2.054 2.832-10.246 20.01-9.729 24.949-2.392 4.608 6.837.757 17.618 2.048 27.686zm-22.216-7.866c4.483 6.856 17.435 2.377 16.751-6.154-5.161-3.469-18.501-3.389-16.751 6.154zM416.873 53.029c-7.868.794-17.201.117-25.638.343-1.48 10.76 16.123 14.618 19.144 5.127 1.631.754 3.326 1.457 5.127 2.048-2.477 9.824-18.37 11.251-25.294 4.445-9.549-9.386-4.276-31.335 12.987-29.735 8.89.826 13.149 7.176 13.674 17.772zm-25.295-4.444h18.801c-.04-11.168-18.433-9.957-18.801 0zM347.486 36.283v32.13h-5.81v-32.13h5.81zM352.273 36.283h6.153c3.048 8.342 6.48 16.303 9.224 24.949 4.33-7.408 6.575-16.895 10.251-24.949h6.155c-4.39 10.646-8.865 21.217-12.988 32.13h-6.152c-3.907-11.019-8.635-21.217-12.643-32.13zM427.354 36.225h-5.525v32.111h5.982V48.885s1.845-9.322 11.396-7.021l2.417-5.867s-8.978-2.532-14.155 5.407l-.115-5.179zM322.434 36.225h-5.522v32.111h5.987V48.885s1.84-9.322 11.395-7.021l2.417-5.867s-8.976-2.532-14.159 5.407l-.118-5.179zM304.139 51.998c0 6.579-4.645 11.919-10.372 11.919-5.725 0-10.366-5.34-10.366-11.919 0-6.586 4.642-11.92 10.366-11.92 5.727 0 10.372 5.334 10.372 11.92zm-.107 11.916v4.19h5.742V21.038h-5.812v19.325c-2.789-3.472-6.805-5.649-11.269-5.649-8.424 0-15.253 7.768-15.253 17.341 0 9.576 6.829 17.344 15.253 17.344 4.496 0 8.536-2.21 11.33-5.724l.009.239z" fill="#6F6F6F"/><circle r="4.185" cy="25.306" cx="344.584" fill="#6F6F6F"/><path fill="#6F6F6F" d="M253.751 50.332l13.835-14.078h7.603l-12.337 12.711 13.21 19.321h-7.354l-10.346-14.959-4.738 4.861v10.225h-5.856V21.422h5.856v28.443zM236.855 46.471c-1.762-3.644-5.163-6.109-9.065-6.109-5.713 0-10.348 5.282-10.348 11.799 0 6.524 4.635 11.806 10.348 11.806 3.93 0 7.347-2.496 9.101-6.183l5.394 2.556c-2.779 5.419-8.227 9.097-14.497 9.097-9.083 0-16.451-7.733-16.451-17.275 0-9.537 7.368-17.269 16.451-17.269 6.247 0 11.683 3.653 14.467 9.041l-5.4 2.537zM160.884 26.693v9.747h-5.849v5.479h5.727v17.052s-.37 13.157 15.103 9.383l-1.947-4.995s-7.065 2.434-7.065-3.896V41.919h7.796V36.56h-7.674v-9.866h-6.091v-.001z"/><path fill="#009245" d="M50.794 41.715L27.708 84.812l46.207-.008z"/><path fill="#006837" d="M27.699 84.804L4.833 44.994h45.958z"/><path fill="#39B54A" d="M50.913 45.008H4.833L27.898 5.12H74.031z"/><path fill="#8CC63F" d="M74.031 5.12l23.236 39.84-23.352 39.844-23.002-39.796z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -14,8 +14,8 @@
"description": "Google Stackdriver Datasource for Grafana",
"version": "1.0.0",
"logos": {
"small": "img/stackdriver_logo.png",
"large": "img/stackdriver_logo.png"
"small": "img/stackdriver_logo.svg",
"large": "img/stackdriver_logo.svg"
},
"author": {
"name": "Grafana Project",

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { QueryCtrl } from 'app/plugins/sdk';
import { Target } from './types';
import { StackdriverQuery } from './types';
import { TemplateSrv } from 'app/features/templating/template_srv';
export class StackdriverQueryCtrl extends QueryCtrl {
@ -16,7 +16,7 @@ export class StackdriverQueryCtrl extends QueryCtrl {
this.onExecuteQuery = this.onExecuteQuery.bind(this);
}
onQueryChange(target: Target) {
onQueryChange(target: StackdriverQuery) {
Object.assign(this.target, target);
}

View File

@ -1,7 +1,8 @@
import StackdriverDataSource from '../datasource';
import { metricDescriptors } from './testData';
import moment from 'moment';
import { TemplateSrvStub } from 'test/specs/helpers';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all';
describe('StackdriverDataSource', () => {
const instanceSettings = {
@ -9,7 +10,7 @@ describe('StackdriverDataSource', () => {
defaultProject: 'testproject',
},
};
const templateSrv = new TemplateSrvStub();
const templateSrv = new TemplateSrv();
const timeSrv = {};
describe('when performing testDataSource', () => {
@ -154,15 +155,41 @@ describe('StackdriverDataSource', () => {
});
});
describe('when interpolating a template variable for the filter', () => {
let interpolated;
describe('and is single value variable', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv('filtervalue1');
const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '${test}'], {});
});
it('should replace the variable with the value', () => {
expect(interpolated.length).toBe(3);
expect(interpolated[2]).toBe('filtervalue1');
});
});
describe('and is multi value variable', () => {
beforeEach(() => {
const filterTemplateSrv = initTemplateSrv(['filtervalue1', 'filtervalue2'], true);
const ds = new StackdriverDataSource(instanceSettings, {}, filterTemplateSrv, timeSrv);
interpolated = ds.interpolateFilters(['resource.label.zone', '=~', '[[test]]'], {});
});
it('should replace the variable with a regex expression', () => {
expect(interpolated[2]).toBe('(filtervalue1|filtervalue2)');
});
});
});
describe('when interpolating a template variable for group bys', () => {
let interpolated;
describe('and is single value variable', () => {
beforeEach(() => {
templateSrv.data = {
test: 'groupby1',
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
const groupByTemplateSrv = initTemplateSrv('groupby1');
const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
@ -174,10 +201,8 @@ describe('StackdriverDataSource', () => {
describe('and is multi value variable', () => {
beforeEach(() => {
templateSrv.data = {
test: 'groupby1,groupby2',
};
const ds = new StackdriverDataSource(instanceSettings, {}, templateSrv, timeSrv);
const groupByTemplateSrv = initTemplateSrv(['groupby1', 'groupby2'], true);
const ds = new StackdriverDataSource(instanceSettings, {}, groupByTemplateSrv, timeSrv);
interpolated = ds.interpolateGroupBys(['[[test]]'], {});
});
@ -241,3 +266,19 @@ describe('StackdriverDataSource', () => {
});
});
});
function initTemplateSrv(values: any, multi = false) {
const templateSrv = new TemplateSrv();
templateSrv.init([
new CustomVariable(
{
name: 'test',
current: {
value: values,
},
multi: multi,
},
{}
),
]);
return templateSrv;
}

View File

@ -1,3 +1,5 @@
import { DataQuery } from '@grafana/ui/src/types';
export enum MetricFindQueryTypes {
Services = 'services',
MetricTypes = 'metricTypes',
@ -20,20 +22,22 @@ export interface VariableQueryData {
services: Array<{ value: string; name: string }>;
}
export interface Target {
defaultProject: string;
unit: string;
export interface StackdriverQuery extends DataQuery {
defaultProject?: string;
unit?: string;
metricType: string;
service: string;
service?: string;
refId: string;
crossSeriesReducer: string;
alignmentPeriod: string;
alignmentPeriod?: string;
perSeriesAligner: string;
groupBys: string[];
filters: string[];
aliasBy: string;
groupBys?: string[];
filters?: string[];
aliasBy?: string;
metricKind: string;
valueType: string;
datasourceId?: number;
view?: string;
}
export interface AnnotationTarget {

View File

@ -274,6 +274,28 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
popover.hide();
}
// hide time picker
const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0;
if (timePickerDropDownIsOpen) {
const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0;
const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0;
const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0;
const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0;
if (
targetIsInTimePickerNav ||
targetIsInTimePickerDropDown ||
targetIsDatePickerRowBtn ||
targetIsDatePickerHeaderBtn
) {
return;
}
scope.$apply(() => {
scope.appEvent('closeTimepicker');
});
}
});
},
};

View File

@ -1,6 +1,6 @@
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
// import { createLogger } from 'redux-logger';
import { createLogger } from 'redux-logger';
import sharedReducers from 'app/core/reducers';
import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/state/reducers';
@ -39,7 +39,7 @@ export function configureStore() {
if (process.env.NODE_ENV !== 'production') {
// DEV builds we had the logger middleware
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))));
} else {
setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk))));
}

View File

@ -1,5 +1,14 @@
import { ComponentClass } from 'react';
import { Value } from 'slate';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryHint } from '@grafana/ui';
import {
RawTimeRange,
TimeRange,
DataQuery,
DataSourceSelectItem,
DataSourceApi,
QueryHint,
ExploreStartPageProps,
} from '@grafana/ui';
import { Emitter } from 'app/core/core';
import { LogsModel } from 'app/core/logs_model';
@ -102,7 +111,7 @@ export interface ExploreItemState {
/**
* React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet.
*/
StartPage?: any;
StartPage?: ComponentClass<ExploreStartPageProps>;
/**
* Width used for calculating the graph interval (can't have more datapoints than pixels)
*/
@ -144,10 +153,10 @@ export interface ExploreItemState {
*/
history: HistoryItem[];
/**
* Initial queries for this Explore, e.g., set via URL. Each query will be
* converted to a query row. Query edits should be tracked in `modifiedQueries` though.
* Queries for this Explore, e.g., set via URL. Each query will be
* converted to a query row.
*/
initialQueries: DataQuery[];
queries: DataQuery[];
/**
* True if this Explore area has been initialized.
* Used to distinguish URL state injection versus split view state injection.
@ -162,12 +171,6 @@ export interface ExploreItemState {
* Log query result to be displayed in the logs result viewer.
*/
logsResult?: LogsModel;
/**
* Copy of `initialQueries` that tracks user edits.
* Don't connect this property to a react component as it is updated on every query change.
* Used when running queries. Needs to be reset to `initialQueries` when those are reset as well.
*/
modifiedQueries: DataQuery[];
/**
* Query intervals for graph queries to determine how many datapoints to return.
* Needs to be updated when `datasourceInstance` or `containerWidth` is changed.
@ -229,6 +232,11 @@ export interface ExploreItemState {
* Table model that combines all query table results into a single table.
*/
tableResult?: TableModel;
/**
* React keys for rendering of QueryRows
*/
queryKeys: string[];
}
export interface ExploreUIState {

View File

@ -4,7 +4,7 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0A0A0C;}
.st0{fill:#161719;}
.st1{fill:#E3E2E2;}
</style>
<g>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -5,7 +5,7 @@
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
.st4{fill:url(#SVGID_4_);}

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -4,7 +4,7 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0A0A0C;}
.st0{fill:#161719;}
.st1{fill:#E3E2E2;}
</style>
<g>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -5,7 +5,7 @@
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
</style>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -4,7 +4,7 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0A0A0C;}
.st0{fill:#161719;}
.st1{fill:#E3E2E2;}
</style>
<g>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -5,7 +5,7 @@
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
.st4{fill:url(#SVGID_4_);}

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -4,7 +4,7 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0A0A0C;}
.st0{fill:#161719;}
.st1{fill:#E3E2E2;}
</style>
<path class="st0" d="M94.3,50C94.3,25.6,74.4,5.7,50,5.7S5.7,25.6,5.7,50S25.6,94.3,50,94.3S94.3,74.4,94.3,50z"/>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,7 +5,7 @@
width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#0A0A0C;}
.st1{fill:#161719;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:url(#SVGID_3_);}
</style>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -212,7 +212,7 @@
padding-right: 5px;
}
.panel-editor-tabs {
.panel-editor-tabs, .add-panel-widget__icon {
.gicon-advanced-active {
background-image: url('../img/icons_#{$theme-name}_theme/icon_advanced_active.svg');
}