Commit 6617a382 by ryan

Merge remote-tracking branch 'grafana/master' into all-data-as-table

* grafana/master: (23 commits)
  @grafana/ui - release docs v1 (#15835)
  Minor refactoring of copy tags when saving feature, #15446
  Simple implementation for preserve tags, closes #11627
  Updated prettierignore
  Refactoring of PR #14772
  fix typo in pr template
  add nil/length check when delete old login attempts
  Minor refactoring of new react text panel
  fix allow anonymous server bind for ldap search
  Update README.md
  use pure component
  use replaceVariables
  Catch bad regex exception at controller level
  11780: invalid reg value can cause unexpected behaviour
  update
  Add string quote func
  Remove option used to control within browser
  Add pic into actionCard message
  Add new option to set where to open the message url
  Optimize the Dingding match values format
  ...
parents 229dff75 57018738
...@@ -20,7 +20,7 @@ Fixes # ...@@ -20,7 +20,7 @@ Fixes #
**Release note**: **Release note**:
<!-- <!--
If this is a user facing change and should be mentioned in relase note add it below. If no, just write "NONE" below. If this is a user facing change and should be mentioned in release note add it below. If no, just write "NONE" below.
--> -->
```release-note ```release-note
......
...@@ -5,4 +5,5 @@ pkg/ ...@@ -5,4 +5,5 @@ pkg/
node_modules node_modules
public/vendor/ public/vendor/
vendor/ vendor/
data/
...@@ -7,12 +7,6 @@ ...@@ -7,12 +7,6 @@
Grafana is an open source, feature rich metrics dashboard and graph editor for Grafana is an open source, feature rich metrics dashboard and graph editor for
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
![](https://www.grafanacon.org/2019/images/grafanacon_la_nav-logo.png)
Join us Feb 25-26 in Los Angeles, California for GrafanaCon - a two-day event with talks focused on Grafana and the surrounding open source monitoring ecosystem. Get deep dives into Loki, the Explore workflow and all of the new features of Grafana 6, plus participate in hands on workshops to help you get the most out of your data.
Time is running out - grab your ticket now! http://grafanacon.org
<!--- <!---
![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png) ![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png)
--> -->
......
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
"gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build", "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build",
"gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release", "gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release",
"gui:publish": "cd packages/grafana-ui/dist && npm publish --access public", "gui:publish": "cd packages/grafana-ui/dist && npm publish --access public",
"gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p", "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p --createVersionCommit",
"cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts" "cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts"
}, },
"husky": { "husky": {
......
...@@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra ...@@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra
`npm install @grafana/ui` `npm install @grafana/ui`
## Development
For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead.
## Building @grafana/ui
To build @grafana/ui run `npm run gui:build` script *from Grafana repository root*. The build will be created in `packages/grafana-ui/dist` directory. Following steps from [Development](#development) you can test built package.
## Releasing new version
To release new version run `npm run gui:release` script *from Grafana repository root*. The script will prepare the distribution package as well as prompt you to bump library version and publish it to the NPM registry.
### Automatic version bump
When running `npm run gui:release` package.json file will be automatically updated. Also, package.json file will be commited and pushed to upstream branch.
### Manual version bump
To use `package.json` defined version run `npm run gui:release --usePackageJsonVersion` *from Grafana repository root*.
### Preparing release package without publishing to NPM registry
For testing purposes there is `npm run gui:releasePrepare` task that prepares distribution package without publishing it to the NPM registry.
### V1 release process overview
1. Package is compiled with TSC. Typings are created in `/dist` directory, and the compiled js lands in `/compiled` dir
2. Rollup creates a CommonJS package based on compiled sources, and outputs it to `/dist` directory
3. Readme, changelog and index.js files are moved to `/dist` directory
4. Package version is bumped in both `@grafana/ui` package dir and in dist directory.
5. Version commit is created and pushed to master branch
5. Package is published to npm
## Versioning ## Versioning
To limit the confusion related to @grafana/ui and Grafana versioning we decided to keep the major version in sync between those two. To limit the confusion related to @grafana/ui and Grafana versioning we decided to keep the major version in sync between those two.
This means, that first version of @grafana/ui is taged with 6.0.0-alpha.0 to keep version in sync with Grafana 6.0 release. This means, that first version of @grafana/ui is taged with 6.0.0-alpha.0 to keep version in sync with Grafana 6.0 release.
...@@ -219,8 +219,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo ...@@ -219,8 +219,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
} }
func (a *ldapAuther) serverBind() error { func (a *ldapAuther) serverBind() error {
bindFn := func() error {
return a.conn.Bind(a.server.BindDN, a.server.BindPassword)
}
if a.server.BindPassword == "" {
bindFn = func() error {
return a.conn.UnauthenticatedBind(a.server.BindDN)
}
}
// bind_dn and bind_password to bind // bind_dn and bind_password to bind
if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil { if err := bindFn(); err != nil {
a.log.Info("LDAP initial bind failed, %v", err) a.log.Info("LDAP initial bind failed, %v", err)
if ldapErr, ok := err.(*ldap.Error); ok { if ldapErr, ok := err.(*ldap.Error); ok {
......
...@@ -78,6 +78,69 @@ func TestLdapAuther(t *testing.T) { ...@@ -78,6 +78,69 @@ func TestLdapAuther(t *testing.T) {
}) })
}) })
Convey("serverBind", t, func() {
Convey("Given bind dn and password configured", func() {
conn := &mockLdapConn{}
var actualUsername, actualPassword string
conn.bindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := ldapAuther.serverBind()
So(err, ShouldBeNil)
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn configured", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{
BindDN: "o=users,dc=grafana,dc=org",
},
}
err := ldapAuther.serverBind()
So(err, ShouldBeNil)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
})
Convey("Given empty bind dn and password", func() {
conn := &mockLdapConn{}
unauthenticatedBindWasCalled := false
var actualUsername string
conn.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
ldapAuther := &ldapAuther{
conn: conn,
server: &LdapServerConf{},
}
err := ldapAuther.serverBind()
So(err, ShouldBeNil)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("When translating ldap user to grafana user", t, func() { Convey("When translating ldap user to grafana user", t, func() {
var user1 = &m.User{} var user1 = &m.User{}
......
package notifiers package notifiers
import ( import (
"fmt"
"net/url"
"strings"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
...@@ -8,19 +12,26 @@ import ( ...@@ -8,19 +12,26 @@ import (
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
) )
func init() { const DefaultDingdingMsgType = "link"
alerting.RegisterNotifier(&alerting.NotifierPlugin{ const DingdingOptionsTemplate = `
Type: "dingding",
Name: "DingDing",
Description: "Sends HTTP POST request to DingDing",
Factory: NewDingDingNotifier,
OptionsTemplate: `
<h3 class="page-heading">DingDing settings</h3> <h3 class="page-heading">DingDing settings</h3>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-10">Url</span> <span class="gf-form-label width-10">Url</span>
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input> <input type="text" required class="gf-form-input max-width-70" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">MessageType</span>
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.msgType" ng-options="s for s in ['link','actionCard']" ng-init="ctrl.model.settings.msgType=ctrl.model.settings.msgType || '` + DefaultDingdingMsgType + `'"></select>
</div> </div>
`, `
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "dingding",
Name: "DingDing",
Description: "Sends HTTP POST request to DingDing",
Factory: NewDingDingNotifier,
OptionsTemplate: DingdingOptionsTemplate,
}) })
} }
...@@ -31,8 +42,11 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) ...@@ -31,8 +42,11 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
} }
msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType)
return &DingDingNotifier{ return &DingDingNotifier{
NotifierBase: NewNotifierBase(model), NotifierBase: NewNotifierBase(model),
MsgType: msgType,
Url: url, Url: url,
log: log.New("alerting.notifier.dingding"), log: log.New("alerting.notifier.dingding"),
}, nil }, nil
...@@ -40,8 +54,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) ...@@ -40,8 +54,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
type DingDingNotifier struct { type DingDingNotifier struct {
NotifierBase NotifierBase
Url string MsgType string
log log.Logger Url string
log log.Logger
} }
func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
...@@ -52,6 +67,16 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -52,6 +67,16 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name) this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name)
messageUrl = "" messageUrl = ""
} }
q := url.Values{
"pc_slide": {"false"},
"url": {messageUrl},
}
// Use special link to auto open the message url outside of Dingding
// Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9
messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode()
this.log.Info("messageUrl:" + messageUrl) this.log.Info("messageUrl:" + messageUrl)
message := evalContext.Rule.Message message := evalContext.Rule.Message
...@@ -61,15 +86,39 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -61,15 +86,39 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
message = title message = title
} }
bodyJSON, err := simplejson.NewJson([]byte(`{ for i, match := range evalContext.EvalMatches {
"msgtype": "link", message += fmt.Sprintf("\\n%2d. %s: %s", i+1, match.Metric, match.Value)
"link": { }
"text": "` + message + `",
"title": "` + title + `", var bodyStr string
"picUrl": "` + picUrl + `", if this.MsgType == "actionCard" {
"messageUrl": "` + messageUrl + `" // Embed the pic into the markdown directly because actionCard doesn't have a picUrl field
if picUrl != "" {
message = "![](" + picUrl + ")\\n\\n" + message
} }
}`))
bodyStr = `{
"msgtype": "actionCard",
"actionCard": {
"text": "` + strings.Replace(message, `"`, "'", -1) + `",
"title": "` + strings.Replace(title, `"`, "'", -1) + `",
"singleTitle": "More",
"singleURL": "` + messageUrl + `"
}
}`
} else {
bodyStr = `{
"msgtype": "link",
"link": {
"text": "` + message + `",
"title": "` + title + `",
"picUrl": "` + picUrl + `",
"messageUrl": "` + messageUrl + `"
}
}`
}
bodyJSON, err := simplejson.NewJson([]byte(bodyStr))
if err != nil { if err != nil {
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name) this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
......
...@@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error { ...@@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
return err return err
} }
if result == nil || len(result) == 0 || result[0] == nil {
return nil
}
maxId = toInt64(result[0]["id"]) maxId = toInt64(result[0]["id"])
if maxId == 0 { if maxId == 0 {
......
import kbn from '../utils/kbn';
describe('stringToJsRegex', () => {
it('should parse the valid regex value', () => {
const output = kbn.stringToJsRegex('/validRegexp/');
expect(output).toBeInstanceOf(RegExp);
});
it('should throw error on invalid regex value', () => {
const input = '/etc/hostname';
expect(() => {
kbn.stringToJsRegex(input);
}).toThrow();
});
});
...@@ -234,6 +234,11 @@ kbn.stringToJsRegex = str => { ...@@ -234,6 +234,11 @@ kbn.stringToJsRegex = str => {
} }
const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
if (!match) {
throw new Error(`'${str}' is not a valid regular expression.`);
}
return new RegExp(match[1], match[2]); return new RegExp(match[1], match[2]);
}; };
......
...@@ -16,18 +16,20 @@ const template = ` ...@@ -16,18 +16,20 @@ const template = `
<form name="ctrl.saveForm" class="modal-content" novalidate> <form name="ctrl.saveForm" class="modal-content" novalidate>
<div class="p-t-2"> <div class="p-t-2">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-7">New name</label> <label class="gf-form-label width-8">New name</label>
<input type="text" class="gf-form-input" ng-model="ctrl.clone.title" give-focus="true" required> <input type="text" class="gf-form-input" ng-model="ctrl.clone.title" give-focus="true" required>
</div> </div>
<div class="gf-form"> <folder-picker initial-folder-id="ctrl.folderId"
<folder-picker initial-folder-id="ctrl.folderId"
on-change="ctrl.onFolderChange($folder)" on-change="ctrl.onFolderChange($folder)"
enter-folder-creation="ctrl.onEnterFolderCreation()" enter-folder-creation="ctrl.onEnterFolderCreation()"
exit-folder-creation="ctrl.onExitFolderCreation()" exit-folder-creation="ctrl.onExitFolderCreation()"
enable-create-new="true" enable-create-new="true"
label-class="width-7" label-class="width-8"
dashboard-id="ctrl.clone.id"> dashboard-id="ctrl.clone.id">
</folder-picker> </folder-picker>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label="Copy tags" label-class="width-8" checked="ctrl.copyTags">
</gf-form-switch>
</div> </div>
</div> </div>
...@@ -44,6 +46,7 @@ export class SaveDashboardAsModalCtrl { ...@@ -44,6 +46,7 @@ export class SaveDashboardAsModalCtrl {
folderId: any; folderId: any;
dismiss: () => void; dismiss: () => void;
isValidFolderSelection = true; isValidFolderSelection = true;
copyTags: boolean;
/** @ngInject */ /** @ngInject */
constructor(private dashboardSrv) { constructor(private dashboardSrv) {
...@@ -55,6 +58,7 @@ export class SaveDashboardAsModalCtrl { ...@@ -55,6 +58,7 @@ export class SaveDashboardAsModalCtrl {
this.clone.editable = true; this.clone.editable = true;
this.clone.hideControls = false; this.clone.hideControls = false;
this.folderId = dashboard.meta.folderId; this.folderId = dashboard.meta.folderId;
this.copyTags = false;
// remove alerts if source dashboard is already persisted // remove alerts if source dashboard is already persisted
// do not want to create alert dupes // do not want to create alert dupes
...@@ -71,6 +75,10 @@ export class SaveDashboardAsModalCtrl { ...@@ -71,6 +75,10 @@ export class SaveDashboardAsModalCtrl {
} }
save() { save() {
if (!this.copyTags) {
this.clone.tags = [];
}
return this.dashboardSrv.save(this.clone, { folderId: this.folderId }).then(this.dismiss); return this.dashboardSrv.save(this.clone, { folderId: this.folderId }).then(this.dismiss);
} }
......
// Libraries
import React, { PureComponent } from 'react';
import Remarkable from 'remarkable';
import { debounce } from 'lodash';
// Utils
import { sanitize } from 'app/core/utils/text';
import config from 'app/core/config';
// Types
import { TextOptions } from './types';
import { PanelProps } from '@grafana/ui/src/types';
interface Props extends PanelProps<TextOptions> {}
interface State {
html: string;
}
export class TextPanel extends PureComponent<Props, State> {
remarkable: Remarkable;
constructor(props) {
super(props);
this.state = {
html: this.processContent(props.options),
};
}
updateHTML = debounce(() => {
const html = this.processContent(this.props.options);
if (html !== this.state.html) {
this.setState({ html });
}
}, 150);
componentDidUpdate(prevProps: Props) {
// Since any change could be referenced in a template variable,
// This needs to process everytime (with debounce)
this.updateHTML();
}
prepareHTML(html: string): string {
const { replaceVariables } = this.props;
html = config.disableSanitizeHtml ? html : sanitize(html);
return replaceVariables(html);
}
prepareText(content: string): string {
return this.prepareHTML(
content
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/\n/g, '<br/>')
);
}
prepareMarkdown(content: string): string {
if (!this.remarkable) {
this.remarkable = new Remarkable();
}
return this.prepareHTML(this.remarkable.render(content));
}
processContent(options: TextOptions): string {
const { mode, content } = options;
if (!content) {
return '';
}
if (mode === 'markdown') {
return this.prepareMarkdown(content);
}
if (mode === 'html') {
return this.prepareHTML(content);
}
return this.prepareText(content);
}
render() {
const { html } = this.state;
return <div className="markdown-html panel-text-content" dangerouslySetInnerHTML={{ __html: html }} />;
}
}
// Libraries
import React, { PureComponent, ChangeEvent } from 'react';
// Components
import { PanelEditorProps, PanelOptionsGroup, Select, SelectOptionItem } from '@grafana/ui';
// Types
import { TextOptions } from './types';
export class TextPanelEditor extends PureComponent<PanelEditorProps<TextOptions>> {
modes: SelectOptionItem[] = [
{ value: 'markdown', label: 'Markdown' },
{ value: 'text', label: 'Text' },
{ value: 'html', label: 'HTML' },
];
onModeChange = (item: SelectOptionItem) => this.props.onOptionsChange({ ...this.props.options, mode: item.value });
onContentChange = (evt: ChangeEvent<HTMLTextAreaElement>) => {
this.props.onOptionsChange({ ...this.props.options, content: (event.target as any).value });
};
render() {
const { mode, content } = this.props.options;
return (
<PanelOptionsGroup title="Text">
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label">Mode</span>
<Select onChange={this.onModeChange} value={this.modes.find(e => mode === e.value)} options={this.modes} />
</div>
</div>
<textarea value={content} onChange={this.onContentChange} className="gf-form-input" rows={10} />
</PanelOptionsGroup>
);
}
}
import React, { PureComponent } from 'react'; import { ReactPanelPlugin } from '@grafana/ui';
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
export class Text2 extends PureComponent<PanelProps> { import { TextPanelEditor } from './TextPanelEditor';
constructor(props: PanelProps) { import { TextPanel } from './TextPanel';
super(props); import { TextOptions, defaults } from './types';
}
render() { export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel);
return <h2>Text Panel!</h2>;
}
}
export const reactPanel = new ReactPanelPlugin(Text2); reactPanel.setEditor(TextPanelEditor);
reactPanel.setDefaults(defaults);
export interface TextOptions {
mode: 'html' | 'markdown' | 'text';
content: string;
}
export const defaults: TextOptions = {
mode: 'markdown',
content: `# Title
For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)
`,
};
...@@ -21,10 +21,8 @@ ...@@ -21,10 +21,8 @@
border-radius: 3px; border-radius: 3px;
text-shadow: none; text-shadow: none;
font-size: 13px; font-size: 13px;
padding: 3px 6px 1px 6px; padding: 2px 6px 2px 6px;
border-width: 1px; border: 1px solid lighten($purple, 10%);
border-style: solid;
box-shadow: 0 0 1px rgba($white, 0.2);
.icon-tag { .icon-tag {
position: relative; position: relative;
......
...@@ -33,10 +33,12 @@ program ...@@ -33,10 +33,12 @@ program
.description('Prepares @grafana/ui release (and publishes to npm on demand)') .description('Prepares @grafana/ui release (and publishes to npm on demand)')
.option('-p, --publish', 'Publish @grafana/ui to npm registry') .option('-p, --publish', 'Publish @grafana/ui to npm registry')
.option('-u, --usePackageJsonVersion', 'Use version specified in package.json') .option('-u, --usePackageJsonVersion', 'Use version specified in package.json')
.option('--createVersionCommit', 'Create and push version commit')
.action(async cmd => { .action(async cmd => {
await execTask(releaseTask)({ await execTask(releaseTask)({
publishToNpm: !!cmd.publish, publishToNpm: !!cmd.publish,
usePackageJsonVersion: !!cmd.usePackageJsonVersion, usePackageJsonVersion: !!cmd.usePackageJsonVersion,
createVersionCommit: !!cmd.createVersionCommit,
}); });
}); });
......
...@@ -9,10 +9,10 @@ import { savePackage, buildTask } from './grafanaui.build'; ...@@ -9,10 +9,10 @@ import { savePackage, buildTask } from './grafanaui.build';
import { TaskRunner, Task } from './task'; import { TaskRunner, Task } from './task';
type VersionBumpType = 'prerelease' | 'patch' | 'minor' | 'major'; type VersionBumpType = 'prerelease' | 'patch' | 'minor' | 'major';
interface ReleaseTaskOptions { interface ReleaseTaskOptions {
publishToNpm: boolean; publishToNpm: boolean;
usePackageJsonVersion: boolean; usePackageJsonVersion: boolean;
createVersionCommit: boolean;
} }
const promptBumpType = async () => { const promptBumpType = async () => {
...@@ -62,6 +62,12 @@ const promptConfirm = async (message?: string) => { ...@@ -62,6 +62,12 @@ const promptConfirm = async (message?: string) => {
]); ]);
}; };
// Since Grafana core depends on @grafana/ui highly, we run full check before release
const runChecksAndTests = async () =>
useSpinner<void>(`Running checks and tests`, async () => {
await execa('npm', ['run', 'test']);
})();
const bumpVersion = (version: string) => const bumpVersion = (version: string) =>
useSpinner<void>(`Saving version ${version} to package.json`, async () => { useSpinner<void>(`Saving version ${version} to package.json`, async () => {
changeCwdToGrafanaUi(); changeCwdToGrafanaUi();
...@@ -94,8 +100,21 @@ const ensureMasterBranch = async () => { ...@@ -94,8 +100,21 @@ const ensureMasterBranch = async () => {
} }
}; };
const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm, usePackageJsonVersion }) => { const prepareVersionCommitAndPush = async (version: string) =>
useSpinner<void>('Commiting and pushing @grafana/ui version update', async () => {
await execa.stdout('git', ['commit', '-a', '-m', `Upgrade @grafana/ui version to v${version}`]);
await execa.stdout('git', ['push']);
})();
const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({
publishToNpm,
usePackageJsonVersion,
createVersionCommit,
}) => {
await runChecksAndTests();
if (publishToNpm) { if (publishToNpm) {
// TODO: Ensure release branch
// When need to update this when we star keeping @grafana/ui releases in sync with core
await ensureMasterBranch(); await ensureMasterBranch();
} }
...@@ -145,10 +164,15 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm, ...@@ -145,10 +164,15 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm,
await bumpVersion(nextVersion); await bumpVersion(nextVersion);
} }
if (createVersionCommit) {
await prepareVersionCommitAndPush(nextVersion);
}
if (publishToNpm) { if (publishToNpm) {
await publishPackage(pkg.name, nextVersion); await publishPackage(pkg.name, nextVersion);
console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`)); console.log(chalk.green(`\nVersion ${nextVersion} of ${pkg.name} succesfully released!`));
console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created - COMMIT THIS FILE!`)); console.log(chalk.yellow(`\nUpdated @grafana/ui/package.json with version bump created.`));
process.exit(); process.exit();
} else { } else {
console.log( console.log(
......
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