Commit fd06e517 by Torkel Ödegaard

Merge branch 'master' into fix/explore-do-not-clear-results

parents d947748d 50b140f6
......@@ -148,9 +148,6 @@ jobs:
name: sha-sum packages
command: 'go run build.go sha-dist'
- run:
name: Build Grafana.com master publisher
command: 'go build -o scripts/publish scripts/build/publish.go'
- run:
name: Test and build Grafana.com release publisher
command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
- persist_to_workspace:
......@@ -158,7 +155,6 @@ jobs:
paths:
- dist/grafana*
- scripts/*.sh
- scripts/publish
- scripts/build/release_publisher/release_publisher
- scripts/build/publish.sh
......@@ -393,7 +389,7 @@ jobs:
name: Publish to Grafana.com
command: |
rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
cd dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -from-local
deploy-release:
docker:
......
......@@ -504,7 +504,7 @@ concurrent_render_limit = 5
#################################### Explore #############################
[explore]
# Enable the Explore section
enabled = false
enabled = true
#################################### Internal Grafana Metrics ############
# Metrics available at HTTP API Url /metrics
......
......@@ -429,7 +429,7 @@ log_queries =
#################################### Explore #############################
[explore]
# Enable the Explore section
;enabled = false
;enabled = true
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /metrics
......
......@@ -336,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
"id": 123123,
"gridPos": map[string]interface{}{
"x": 0,
"y": 0,
"y": 3,
"w": 24,
"h": 4,
},
......
......@@ -718,7 +718,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data")
explore := iniFile.Section("explore")
ExploreEnabled = explore.Key("enabled").MustBool(false)
ExploreEnabled = explore.Key("enabled").MustBool(true)
panels := iniFile.Section("panels")
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
......
import { LocationUpdate } from 'app/types';
export enum CoreActionTypes {
UpdateLocation = 'UPDATE_LOCATION',
}
export type Action = UpdateLocationAction;
export interface UpdateLocationAction {
type: 'UPDATE_LOCATION';
type: CoreActionTypes.UpdateLocation;
payload: LocationUpdate;
}
export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({
type: 'UPDATE_LOCATION',
type: CoreActionTypes.UpdateLocation,
payload: location,
});
import { Action } from 'app/core/actions/location';
import { Action, CoreActionTypes } from 'app/core/actions/location';
import { LocationState } from 'app/types';
import { renderUrl } from 'app/core/utils/url';
import _ from 'lodash';
......@@ -12,7 +12,7 @@ export const initialState: LocationState = {
export const locationReducer = (state = initialState, action: Action): LocationState => {
switch (action.type) {
case 'UPDATE_LOCATION': {
case CoreActionTypes.UpdateLocation: {
const { path, routeParams } = action.payload;
let query = action.payload.query || state.query;
......@@ -24,9 +24,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS
return {
url: renderUrl(path || state.path, query),
path: path || state.path,
query: {
...query,
},
query: { ...query },
routeParams: routeParams || state.routeParams,
};
}
......
......@@ -58,7 +58,6 @@ describe('when updating view state', () => {
it('should remove params from query string', () => {
viewState.update({ fullscreen: true, panelId: 1, edit: true });
viewState.update({ fullscreen: false });
expect(viewState.dashboard.meta.fullscreen).toBe(false);
expect(viewState.state.fullscreen).toBe(null);
});
});
......
......@@ -72,7 +72,6 @@ export class DashboardViewStateSrv {
}
_.extend(this.state, state);
this.dashboard.meta.fullscreen = this.state.fullscreen;
if (!this.state.fullscreen) {
this.state.fullscreen = null;
......@@ -117,10 +116,20 @@ export class DashboardViewStateSrv {
}
syncState() {
if (this.dashboard.meta.fullscreen) {
if (this.state.fullscreen) {
const panel = this.dashboard.getPanelById(this.state.panelId);
if (!panel) {
this.state.fullscreen = null;
this.state.panelId = null;
this.state.edit = null;
this.update(this.state);
setTimeout(() => {
appEvents.emit('alert-error', ['Error', 'Panel not found']);
}, 100);
return;
}
......
......@@ -9,8 +9,6 @@ import { AutoSizer } from 'react-virtualized';
import store from 'app/core/store';
// Components
import { DataSourceSelectItem } from '@grafana/ui/src/types';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { Alert } from './Error';
import ErrorBoundary from './ErrorBoundary';
import GraphContainer from './GraphContainer';
......@@ -21,18 +19,13 @@ import TimePicker, { parseTime } from './TimePicker';
// Actions
import {
changeDatasource,
changeSize,
changeTime,
clearQueries,
initializeExplore,
modifyQueries,
runQueries,
scanStart,
scanStop,
setQueries,
splitClose,
splitOpen,
} from './state/actions';
// Types
......@@ -41,27 +34,23 @@ import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/
import { StoreState } from 'app/types';
import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore';
import { Emitter } from 'app/core/utils/emitter';
import { ExploreToolbar } from './ExploreToolbar';
interface ExploreProps {
StartPage?: any;
changeDatasource: typeof changeDatasource;
changeSize: typeof changeSize;
changeTime: typeof changeTime;
clearQueries: typeof clearQueries;
datasourceError: string;
datasourceInstance: any;
datasourceLoading: boolean | null;
datasourceMissing: boolean;
exploreDatasources: DataSourceSelectItem[];
exploreId: ExploreId;
initialDatasource?: string;
initialQueries: DataQuery[];
initializeExplore: typeof initializeExplore;
initialized: boolean;
loading: boolean;
modifyQueries: typeof modifyQueries;
range: RawTimeRange;
runQueries: typeof runQueries;
scanner?: RangeScanner;
scanning?: boolean;
scanRange?: RawTimeRange;
......@@ -69,8 +58,6 @@ interface ExploreProps {
scanStop: typeof scanStop;
setQueries: typeof setQueries;
split: boolean;
splitClose: typeof splitClose;
splitOpen: typeof splitOpen;
showingStartPage?: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
......@@ -145,10 +132,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.el = el;
};
onChangeDatasource = async option => {
this.props.changeDatasource(this.props.exploreId, option.value);
};
onChangeTime = (range: TimeRange, changedByScanner?: boolean) => {
if (this.props.scanning && !changedByScanner) {
this.onStopScanning();
......@@ -156,23 +139,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.props.changeTime(this.props.exploreId, range);
};
onClickClear = () => {
this.props.clearQueries(this.props.exploreId);
};
onClickCloseSplit = () => {
this.props.splitClose();
};
// Use this in help pages to set page to a single query
onClickExample = (query: DataQuery) => {
this.props.setQueries(this.props.exploreId, [query]);
};
onClickSplit = () => {
this.props.splitOpen();
};
onClickLabel = (key: string, value: string) => {
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
};
......@@ -204,10 +175,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
this.props.scanStop(this.props.exploreId);
};
onSubmit = () => {
this.props.runQueries(this.props.exploreId);
};
render() {
const {
StartPage,
......@@ -215,11 +182,8 @@ export class Explore extends React.PureComponent<ExploreProps> {
datasourceError,
datasourceLoading,
datasourceMissing,
exploreDatasources,
exploreId,
loading,
initialQueries,
range,
showingStartPage,
split,
supportsGraph,
......@@ -227,64 +191,10 @@ export class Explore extends React.PureComponent<ExploreProps> {
supportsTable,
} = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore';
const selectedDatasource = datasourceInstance
? exploreDatasources.find(d => d.name === datasourceInstance.name)
: undefined;
return (
<div className={exploreClass} ref={this.getRef}>
<div className="navbar">
{exploreId === 'left' ? (
<div>
<a className="navbar-page-btn">
<i className="fa fa-rocket" />
Explore
</a>
</div>
) : (
<>
<div className="navbar-page-btn" />
<div className="navbar-buttons explore-first-button">
<button className="btn navbar-button" onClick={this.onClickCloseSplit}>
Close Split
</button>
</div>
</>
)}
{!datasourceMissing ? (
<div className="navbar-buttons">
<DataSourcePicker
onChange={this.onChangeDatasource}
datasources={exploreDatasources}
current={selectedDatasource}
/>
</div>
) : null}
<div className="navbar__spacer" />
{exploreId === 'left' && !split ? (
<div className="navbar-buttons">
<button className="btn navbar-button" onClick={this.onClickSplit}>
Split
</button>
</div>
) : null}
<TimePicker ref={this.timepickerRef} range={range} onChangeTime={this.onChangeTime} />
<div className="navbar-buttons">
<button className="btn navbar-button navbar-button--no-icon" onClick={this.onClickClear}>
Clear All
</button>
</div>
<div className="navbar-buttons relative">
<button className="btn navbar-button navbar-button--primary" onClick={this.onSubmit}>
Run Query{' '}
{loading ? (
<i className="fa fa-spinner fa-fw fa-spin run-icon" />
) : (
<i className="fa fa-level-down fa-fw run-icon" />
)}
</button>
</div>
</div>
<ExploreToolbar exploreId={exploreId} timepickerRef={this.timepickerRef} onChangeTime={this.onChangeTime} />
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
{datasourceMissing ? (
<div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
......@@ -341,30 +251,24 @@ function mapStateToProps(state: StoreState, { exploreId }) {
datasourceInstance,
datasourceLoading,
datasourceMissing,
exploreDatasources,
initialDatasource,
initialQueries,
initialized,
queryTransactions,
range,
showingStartPage,
supportsGraph,
supportsLogs,
supportsTable,
} = item;
const loading = queryTransactions.some(qt => !qt.done);
return {
StartPage,
datasourceError,
datasourceInstance,
datasourceLoading,
datasourceMissing,
exploreDatasources,
initialDatasource,
initialQueries,
initialized,
loading,
queryTransactions,
range,
showingStartPage,
split,
......@@ -375,18 +279,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
}
const mapDispatchToProps = {
changeDatasource,
changeSize,
changeTime,
clearQueries,
initializeExplore,
modifyQueries,
runQueries,
scanStart,
scanStop,
setQueries,
splitClose,
splitOpen,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore));
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { ExploreId } from 'app/types/explore';
import { DataSourceSelectItem, RawTimeRange, TimeRange } from '@grafana/ui';
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';
enum IconSide {
left = 'left',
right = 'right',
}
const createResponsiveButton = (options: {
splitted: boolean;
title: string;
onClick: () => void;
buttonClassName?: string;
iconClassName?: string;
iconSide?: IconSide;
}) => {
const defaultOptions = {
iconSide: IconSide.left,
};
const props = { ...options, defaultOptions };
const { title, onClick, buttonClassName, iconClassName, splitted, iconSide } = props;
return (
<button className={`btn navbar-button ${buttonClassName ? buttonClassName : ''}`} onClick={onClick}>
{iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName} icon-margin-right`} /> : null}
<span className="btn-title">{!splitted ? title : ''}</span>
{iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName} icon-margin-left`} /> : null}
</button>
);
};
interface OwnProps {
exploreId: ExploreId;
timepickerRef: React.RefObject<TimePicker>;
onChangeTime: (range: TimeRange, changedByScanner?: boolean) => void;
}
interface StateProps {
datasourceMissing: boolean;
exploreDatasources: DataSourceSelectItem[];
loading: boolean;
range: RawTimeRange;
selectedDatasource: DataSourceSelectItem;
splitted: boolean;
}
interface DispatchProps {
changeDatasource: typeof changeDatasource;
clearAll: typeof clearQueries;
runQuery: typeof runQueries;
closeSplit: typeof splitClose;
split: typeof splitOpen;
}
type Props = StateProps & DispatchProps & OwnProps;
export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
constructor(props) {
super(props);
}
onChangeDatasource = async option => {
this.props.changeDatasource(this.props.exploreId, option.value);
};
onClearAll = () => {
this.props.clearAll(this.props.exploreId);
};
onRunQuery = () => {
this.props.runQuery(this.props.exploreId);
};
render() {
const {
datasourceMissing,
exploreDatasources,
exploreId,
loading,
range,
selectedDatasource,
splitted,
timepickerRef,
} = this.props;
return (
<div className={splitted ? 'explore-toolbar splitted' : 'explore-toolbar'}>
<div className="explore-toolbar-item">
<div className="explore-toolbar-header">
<div className="explore-toolbar-header-title">
{exploreId === 'left' && (
<a className="navbar-page-btn">
<i className="fa fa-rocket fa-fw" />
Explore
</a>
)}
</div>
<div className="explore-toolbar-header-close">
{exploreId === 'right' && (
<a onClick={this.props.closeSplit}>
<i className="fa fa-times fa-fw" />
</a>
)}
</div>
</div>
</div>
<div className="explore-toolbar-item">
<div className="explore-toolbar-content">
{!datasourceMissing ? (
<div className="explore-toolbar-content-item">
<div className="datasource-picker">
<DataSourcePicker
onChange={this.onChangeDatasource}
datasources={exploreDatasources}
current={selectedDatasource}
/>
</div>
</div>
) : null}
{exploreId === 'left' && !splitted ? (
<div className="explore-toolbar-content-item">
{createResponsiveButton({
splitted,
title: 'Split',
onClick: this.props.split,
iconClassName: 'fa fa-fw fa-columns icon-margin-right',
iconSide: IconSide.left,
})}
</div>
) : null}
<div className="explore-toolbar-content-item timepicker">
<TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
</div>
<div className="explore-toolbar-content-item">
<button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>
Clear All
</button>
</div>
<div className="explore-toolbar-content-item">
{createResponsiveButton({
splitted,
title: 'Run Query',
onClick: this.onRunQuery,
buttonClassName: 'navbar-button--primary',
iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon',
iconSide: IconSide.right,
})}
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
const splitted = state.explore.split;
const exploreItem = state.explore[exploreId];
const { datasourceInstance, datasourceMissing, exploreDatasources, queryTransactions, range } = exploreItem;
const selectedDatasource = datasourceInstance
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
: undefined;
const loading = queryTransactions.some(qt => !qt.done);
return {
datasourceMissing,
exploreDatasources,
loading,
range,
selectedDatasource,
splitted,
};
};
const mapDispatchToProps: DispatchProps = {
changeDatasource,
clearAll: clearQueries,
runQuery: runQueries,
closeSplit: splitClose,
split: splitOpen,
};
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));
......@@ -73,6 +73,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
placeholdersBuffer: PlaceholdersBuffer;
plugins: any[];
resetTimer: any;
mounted: boolean;
constructor(props: QueryFieldProps, context) {
super(props, context);
......@@ -93,10 +94,12 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
}
componentDidMount() {
this.mounted = true;
this.updateMenu();
}
componentWillUnmount() {
this.mounted = false;
clearTimeout(this.resetTimer);
}
......@@ -347,13 +350,15 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
};
resetTypeahead = () => {
this.setState({
suggestions: [],
typeaheadIndex: 0,
typeaheadPrefix: '',
typeaheadContext: null,
});
this.resetTimer = null;
if (this.mounted) {
this.setState({
suggestions: [],
typeaheadIndex: 0,
typeaheadPrefix: '',
typeaheadContext: null,
});
this.resetTimer = null;
}
};
handleBlur = () => {
......
......@@ -293,6 +293,7 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
render() {
const { isUtc, rangeString, refreshInterval } = this.state;
return (
<div className="timepicker">
<div className="navbar-buttons">
......
......@@ -7,7 +7,7 @@ import { StoreState } from 'app/types';
import { ExploreId, ExploreUrlState } from 'app/types/explore';
import { parseUrlState } from 'app/core/utils/explore';
import { initializeExploreSplit } from './state/actions';
import { initializeExploreSplit, resetExplore } from './state/actions';
import ErrorBoundary from './ErrorBoundary';
import Explore from './Explore';
import { CustomScrollbar } from '@grafana/ui';
......@@ -16,6 +16,7 @@ interface WrapperProps {
initializeExploreSplit: typeof initializeExploreSplit;
split: boolean;
updateLocation: typeof updateLocation;
resetExplore: typeof resetExplore;
urlStates: { [key: string]: string };
}
......@@ -42,6 +43,10 @@ export class Wrapper extends Component<WrapperProps> {
}
}
componentWillUnmount() {
this.props.resetExplore();
}
render() {
const { split } = this.props;
const { leftState, rightState } = this.urlStates;
......@@ -74,6 +79,7 @@ const mapStateToProps = (state: StoreState) => {
const mapDispatchToProps = {
initializeExploreSplit,
updateLocation,
resetExplore,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper));
// Types
import { Emitter } from 'app/core/core';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem } from '@grafana/ui/src/types';
import {
ExploreId,
ExploreItemState,
......@@ -41,6 +41,7 @@ export enum ActionTypes {
ToggleGraph = 'explore/TOGGLE_GRAPH',
ToggleLogs = 'explore/TOGGLE_LOGS',
ToggleTable = 'explore/TOGGLE_TABLE',
ResetExplore = 'explore/RESET_EXPLORE',
}
export interface AddQueryRowAction {
......@@ -270,6 +271,11 @@ export interface ToggleLogsAction {
};
}
export interface ResetExploreAction {
type: ActionTypes.ResetExplore;
payload: {};
}
export type Action =
| AddQueryRowAction
| ChangeQueryAction
......@@ -297,4 +303,5 @@ export type Action =
| SplitOpenAction
| ToggleGraphAction
| ToggleLogsAction
| ToggleTableAction;
| ToggleTableAction
| ResetExploreAction;
......@@ -21,7 +21,7 @@ import { updateLocation } from 'app/core/actions';
// Types
import { StoreState } from 'app/types';
import { DataQuery, DataSourceSelectItem, QueryHint } from '@grafana/ui/src/types';
import { DataQuery, DataSourceSelectItem, QueryHint } from '@grafana/ui/src/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
ExploreId,
......@@ -48,7 +48,6 @@ import {
ScanStopAction,
} from './actionTypes';
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
/**
......@@ -539,6 +538,7 @@ export function runQueries(exploreId: ExploreId) {
if (!hasNonEmptyQuery(modifiedQueries)) {
dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
dispatch(stateSave()); // Remember to saves to state and update location
return;
}
......@@ -766,3 +766,12 @@ export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
}
};
}
/**
* Resets state for explore.
*/
export function resetExplore(): ThunkResult<void> {
return dispatch => {
dispatch({ type: ActionTypes.ResetExplore, payload: {} });
};
}
......@@ -422,25 +422,19 @@ export const itemReducer = (state, action: Action): ExploreItemState => {
export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
switch (action.type) {
case ActionTypes.SplitClose: {
return {
...state,
split: false,
};
return { ...state, split: false };
}
case ActionTypes.SplitOpen: {
return {
...state,
split: true,
right: action.payload.itemState,
};
return { ...state, split: true, right: action.payload.itemState };
}
case ActionTypes.InitializeExploreSplit: {
return {
...state,
split: true,
};
return { ...state, split: true };
}
case ActionTypes.ResetExplore: {
return initialExploreState;
}
}
......
......@@ -173,8 +173,9 @@ export default class LokiLanguageProvider extends LanguageProvider {
})
);
}
// Return a cleaned LokiQuery
return queries.map(query => ({
...query,
refId: query.refId,
expr: '',
}));
}
......
......@@ -11,6 +11,23 @@
"links": [],
"panels": [
{
"content": "<div class=\"text-center dashboard-header\">\n <span>Home Dashboard</span>\n</div>",
"editable": true,
"id": 1,
"links": [],
"mode": "html",
"style": {},
"title": "",
"transparent": true,
"type": "text",
"gridPos": {
"w": 24,
"h": 3,
"x": 0,
"y": 0
}
},
{
"folderId": 0,
"headings": true,
"id": 3,
......@@ -28,7 +45,7 @@
"w": 12,
"h": 17,
"x": 0,
"y": 1
"y": 6
}
},
{
......@@ -43,7 +60,7 @@
"w": 12,
"h": 17,
"x": 12,
"y": 1
"y": 6
}
}
],
......
.explore {
flex: 1 1 auto;
.icon-margin-right {
margin-right: 0.25em;
}
&-container {
padding: $dashboard-padding;
}
.icon-margin-left {
margin-left: 0.25em;
}
&-wrapper {
display: flex;
.run-icon {
transform: rotate(90deg);
}
> .explore-split {
width: 50%;
}
}
.timepicker {
display: flex;
}
// Push split button a bit
.explore-first-button {
margin-left: 15px;
.timepicker-rangestring {
margin-left: 0.5em;
}
.datasource-picker {
.ds-picker {
min-width: 200px;
max-width: 200px;
}
}
.explore-panel {
margin-top: $panel-margin;
.sidemenu-open {
.explore-toolbar-header {
padding: 0;
margin-left: 0;
}
.explore-panel__body {
padding: $panel-padding;
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
}
}
}
.explore-panel__header {
padding: $panel-padding;
padding-top: 5px;
padding-bottom: 0;
display: flex;
cursor: pointer;
margin-bottom: 5px;
transition: all 0.1s linear;
.explore-toolbar {
background: inherit;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
height: auto;
padding: 0px $dashboard-padding;
border-bottom: 1px solid #0000;
transition-duration: 0.35s;
transition-timing-function: ease-in-out;
transition-property: box-shadow, border-bottom;
}
.explore-toolbar-item {
position: relative;
align-self: center;
}
.explore-toolbar.splitted {
.explore-toolbar-item {
flex: 1 1 100%;
}
.explore-panel__header-label {
font-weight: 500;
margin-right: $panel-margin;
font-size: $font-size-h6;
box-shadow: $text-shadow-faint;
.explore-toolbar-content-item:first-child {
padding-left: 0;
margin-right: auto;
}
}
.explore-toolbar-item:last-child {
flex: auto;
}
.explore-panel__header-buttons {
margin-right: $panel-margin;
font-size: $font-size-lg;
line-height: $font-size-h6;
.explore-toolbar-header {
display: flex;
flex: 1 1 0;
flex-flow: row nowrap;
font-size: 18px;
min-height: 55px;
line-height: 55px;
justify-content: space-between;
margin-left: $panel-margin * 3;
}
.explore-toolbar-header {
justify-content: space-between;
align-items: center;
}
.explore-toolbar-header-title {
color: darken($link-color, 5%);
.navbar-page-btn {
padding-left: $dashboard-padding;
}
// Make sure wrap buttons around on small screens
.navbar {
flex-wrap: wrap;
height: auto;
.fa {
font-size: 100%;
opacity: 0.75;
margin-right: 0.5em;
}
}
.navbar-page-btn {
margin-right: 1rem;
.explore-toolbar-header-close {
margin-left: auto;
}
.explore-toolbar-content {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
}
.explore-toolbar-content-item {
padding: 10px 2px;
}
.explore-toolbar-content-item:first-child {
padding-left: $dashboard-padding;
margin-right: auto;
}
// Explore icon in header
.fa {
font-size: 100%;
opacity: 0.75;
margin-right: 0.5em;
@media only screen and (max-width: 1545px) {
.explore-toolbar.splitted {
.timepicker-rangestring {
display: none;
}
}
}
// Toggle mode
.navbar-button.active {
color: $btn-active-text-color;
background-color: $btn-active-bg;
@media only screen and (max-width: 1070px) {
.timepicker {
.timepicker-rangestring {
display: none;
}
}
.navbar-button--no-icon {
line-height: 18px;
.explore-toolbar-content {
justify-content: flex-start;
}
.result-options {
margin: 2 * $panel-margin 0;
.explore-toolbar.splitted {
.explore-toolbar-content-item {
padding: 2px 0;
margin: 0;
}
}
.time-series-disclaimer {
width: 300px;
margin: $panel-margin auto;
padding: 10px 0;
border-radius: $border-radius;
text-align: center;
background-color: $panel-bg;
.disclaimer-icon {
color: $yellow;
margin-right: $panel-margin/2;
}
.explore-toolbar-content-item {
padding: 2px 2px;
}
}
.show-all-time-series {
cursor: pointer;
color: $external-link-color;
@media only screen and (max-width: 803px) {
.sidemenu-open {
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: 0;
}
}
}
.navbar .elapsed-time {
position: absolute;
left: 0;
right: 0;
top: 3.5rem;
text-align: center;
font-size: 0.8rem;
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: $dashboard-padding;
}
}
.graph-legend {
flex-wrap: wrap;
.btn-title {
display: none;
}
}
.explore-panel__loader {
height: 2px;
position: relative;
overflow: hidden;
background: none;
margin: $panel-margin / 2;
@media only screen and (max-width: 702px) {
.explore-toolbar-content-item:first-child {
padding-left: 2px;
margin-right: 0;
}
}
.explore-panel__loader--active:after {
content: ' ';
display: block;
width: 25%;
top: 0;
top: -50%;
height: 250%;
position: absolute;
animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms;
animation-iteration-count: 100;
left: -25%;
background: $blue;
@media only screen and (max-width: 544px) {
.sidemenu-open {
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: $dashboard-padding;
}
}
}
@keyframes loader {
from {
left: -25%;
opacity: .1;
}
to {
opacity: 1;
left: 100%;
.explore-toolbar-header-title {
.navbar-page-btn {
padding-left: 0;
margin-left: $dashboard-padding;
}
}
}
.datasource-picker {
min-width: 200px;
}
.explore {
flex: 1 1 auto;
}
.timepicker {
display: flex;
.explore + .explore {
border-left: 1px dotted $table-border;
}
&-rangestring {
margin-left: 0.5em;
}
}
.explore-container {
padding: $dashboard-padding;
}
.explore-wrapper {
display: flex;
.run-icon {
margin-left: 0.25em;
transform: rotate(90deg);
> .explore-split {
width: 50%;
}
}
.explore-panel {
margin-top: $panel-margin;
}
.explore-panel__body {
padding: $panel-padding;
}
.explore-panel__header {
padding: $panel-padding;
padding-top: 5px;
padding-bottom: 0;
display: flex;
cursor: pointer;
margin-bottom: 5px;
transition: all 0.1s linear;
}
.relative {
position: relative;
.explore-panel__header-label {
font-weight: 500;
margin-right: $panel-margin;
font-size: $font-size-h6;
box-shadow: $text-shadow-faint;
}
.explore-panel__header-buttons {
margin-right: $panel-margin;
font-size: $font-size-lg;
line-height: $font-size-h6;
}
.result-options {
margin: 2 * $panel-margin 0;
}
.time-series-disclaimer {
width: 300px;
margin: $panel-margin auto;
padding: 10px 0;
border-radius: $border-radius;
text-align: center;
background-color: $panel-bg;
.disclaimer-icon {
color: $yellow;
margin-right: $panel-margin/2;
}
.link {
text-decoration: underline;
.show-all-time-series {
cursor: pointer;
color: $external-link-color;
}
}
.explore + .explore {
border-left: 1px dotted $table-border;
.navbar .elapsed-time {
position: absolute;
left: 0;
right: 0;
top: 3.5rem;
text-align: center;
font-size: 0.8rem;
}
.graph-legend {
flex-wrap: wrap;
}
.explore-panel__loader {
height: 2px;
position: relative;
overflow: hidden;
background: none;
margin: $panel-margin / 2;
}
.explore-panel__loader--active:after {
content: ' ';
display: block;
width: 25%;
top: 0;
top: -50%;
height: 250%;
position: absolute;
animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms;
animation-iteration-count: 100;
left: -25%;
background: $blue;
}
@keyframes loader {
from {
left: -25%;
opacity: 0.1;
}
to {
left: 100%;
opacity: 1;
}
}
.query-row {
......
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
var apiURL = flag.String("apiUrl", "https://grafana.com/api", "api url")
var apiKey = flag.String("apiKey", "", "api key")
var version = ""
var versionRe = regexp.MustCompile(`grafana-(.*)(\.|_)(arm64|armhfp|aarch64|armv7|darwin|linux|windows|x86_64)`)
var debVersionRe = regexp.MustCompile(`grafana_(.*)_(arm64|armv7|armhf|amd64)\.deb`)
var builds = []build{}
var architectureMapping = map[string]string{
"armv7": "armv7",
"armhfp": "armv7",
"armhf": "armv7",
"arm64": "arm64",
"aarch64": "arm64",
"amd64": "amd64",
"x86_64": "amd64",
}
func main() {
flag.Parse()
if *apiKey == "" {
log.Fatalf("Require apiKey command line parameters")
}
err := filepath.Walk("dist", packageWalker)
if err != nil {
log.Fatalf("Cannot find any packages to publish, %v", err)
}
if version == "" {
log.Fatalf("No version found")
}
if len(builds) == 0 {
log.Fatalf("No builds found")
}
nightly := release{
Version: version,
ReleaseDate: time.Now(),
Stable: false,
Nightly: true,
Beta: false,
WhatsNewURL: "",
ReleaseNotesURL: "",
Builds: builds,
}
postRequest("/grafana/versions", nightly, fmt.Sprintf("Create Release %s", nightly.Version))
postRequest("/grafana/versions/"+nightly.Version, nightly, fmt.Sprintf("Update Release %s", nightly.Version))
for _, b := range nightly.Builds {
postRequest(fmt.Sprintf("/grafana/versions/%s/packages", nightly.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch))
postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", nightly.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch))
}
}
func mapPackage(path string, name string, shaBytes []byte) (build, error) {
log.Printf("Finding package file %s", name)
result := versionRe.FindSubmatch([]byte(name))
debResult := debVersionRe.FindSubmatch([]byte(name))
if len(result) > 0 {
version = string(result[1])
log.Printf("Version detected: %v", version)
} else if len(debResult) > 0 {
version = string(debResult[1])
} else {
return build{}, fmt.Errorf("Unable to figure out version from '%v'", name)
}
os := ""
if strings.Contains(name, "linux") {
os = "linux"
}
if strings.HasSuffix(name, "windows-amd64.zip") {
os = "win"
}
if strings.HasSuffix(name, "darwin-amd64.tar.gz") {
os = "darwin"
}
if strings.HasSuffix(name, ".rpm") {
os = "rhel"
}
if strings.HasSuffix(name, ".deb") {
os = "deb"
}
if os == "" {
return build{}, fmt.Errorf("Unable to figure out os from '%v'", name)
}
arch := ""
for archListed, archReal := range architectureMapping {
if strings.Contains(name, archListed) {
arch = archReal
break
}
}
if arch == "" {
return build{}, fmt.Errorf("Unable to figure out arch from '%v'", name)
}
return build{
Os: os,
Arch: arch,
URL: "https://s3-us-west-2.amazonaws.com/grafana-releases/master/" + name,
Sha256: string(shaBytes),
}, nil
}
func packageWalker(path string, f os.FileInfo, err error) error {
if err != nil {
log.Printf("error: %v", err)
}
if f.Name() == "dist" || strings.Contains(f.Name(), "sha256") || strings.Contains(f.Name(), "latest") {
return nil
}
shaBytes, err := ioutil.ReadFile(path + ".sha256")
if err != nil {
log.Fatalf("Failed to read sha256 file %v", err)
}
build, err := mapPackage(path, f.Name(), shaBytes)
if err != nil {
log.Printf("Could not map metadata from package: %v", err)
return nil
}
builds = append(builds, build)
return nil
}
func postRequest(url string, obj interface{}, desc string) {
jsonBytes, _ := json.Marshal(obj)
req, _ := http.NewRequest(http.MethodPost, (*apiURL)+url, bytes.NewReader(jsonBytes))
req.Header.Add("Authorization", "Bearer "+(*apiKey))
req.Header.Add("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("error: %v", err)
}
if res.StatusCode == http.StatusOK {
log.Printf("Action: %s \t OK", desc)
} else {
if res.Body != nil {
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
if strings.Contains(string(body), "already exists") || strings.Contains(string(body), "Nothing to update") {
log.Printf("Action: %s \t Already exists", desc)
} else {
log.Printf("Action: %s \t Failed - Status: %v", desc, res.Status)
log.Printf("Resp: %s", body)
log.Fatalf("Quitting")
}
}
}
}
type release struct {
Version string `json:"version"`
ReleaseDate time.Time `json:"releaseDate"`
Stable bool `json:"stable"`
Beta bool `json:"beta"`
Nightly bool `json:"nightly"`
WhatsNewURL string `json:"whatsNewUrl"`
ReleaseNotesURL string `json:"releaseNotesUrl"`
Builds []build `json:"-"`
}
type build struct {
Os string `json:"os"`
URL string `json:"url"`
Sha256 string `json:"sha256"`
Arch string `json:"arch"`
}
......@@ -128,12 +128,22 @@ var completeBuildArtifactConfigurations = []buildArtifact{
urlPostfix: "_armhf.deb",
},
{
os: "deb",
arch: "armv6",
urlPostfix: "_armel.deb",
},
{
os: "rhel",
arch: "armv7",
urlPostfix: ".armhfp.rpm",
},
{
os: "linux",
arch: "armv6",
urlPostfix: ".linux-armv6.tar.gz",
},
{
os: "linux",
arch: "armv7",
urlPostfix: ".linux-armv7.tar.gz",
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment