mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'grafana/master' into alpha-react-virtualized-table
* grafana/master: (24 commits) docs: Fix indentation level for OAuth2 config docs: update CONTRIBUTING.md @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 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 ...
This commit is contained in:
commit
343e49decb
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -20,7 +20,7 @@ Fixes #
|
||||
|
||||
**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
|
||||
|
||||
|
@ -5,4 +5,5 @@ pkg/
|
||||
node_modules
|
||||
public/vendor/
|
||||
vendor/
|
||||
data/
|
||||
|
||||
|
@ -34,10 +34,10 @@ To setup a local development environment we recommend reading [Building Grafana
|
||||
### Pull requests with new features
|
||||
Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
|
||||
|
||||
Make sure to include `closes #<issue>` or `fixes #<issue>` in the pull request description.
|
||||
Make sure to include `Closes #<issue number>` or `Fixes #<issue number>` in the pull request description.
|
||||
|
||||
### Pull requests with bug fixes
|
||||
Please make all changes in one commit if possible. Include `closes #12345` in bottom of the commit message.
|
||||
Please make all changes in one commit if possible. Include `Closes #<issue number>` in bottom of the commit message.
|
||||
A commit message for a bug fix should look something like this.
|
||||
|
||||
```
|
||||
@ -48,7 +48,7 @@ provsioners each provisioner overwrite each other.
|
||||
filling up dashboard_versions quite fast if using
|
||||
default settings.
|
||||
|
||||
closes #12864
|
||||
Closes #12864
|
||||
```
|
||||
|
||||
If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged.
|
||||
If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged.
|
||||
|
@ -217,10 +217,10 @@ Some OAuth2 providers might not support `client_id` and `client_secret` passed v
|
||||
results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be
|
||||
send via POST body, which can be enabled via the following settings:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
send_client_credentials_via_post = true
|
||||
```
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
send_client_credentials_via_post = true
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -139,7 +139,7 @@
|
||||
"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: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"
|
||||
},
|
||||
"husky": {
|
||||
|
@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra
|
||||
|
||||
`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
|
||||
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.
|
||||
|
||||
|
||||
|
@ -219,8 +219,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
|
@ -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() {
|
||||
|
||||
var user1 = &m.User{}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
@ -8,19 +12,26 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "dingding",
|
||||
Name: "DingDing",
|
||||
Description: "Sends HTTP POST request to DingDing",
|
||||
Factory: NewDingDingNotifier,
|
||||
OptionsTemplate: `
|
||||
const DefaultDingdingMsgType = "link"
|
||||
const DingdingOptionsTemplate = `
|
||||
<h3 class="page-heading">DingDing settings</h3>
|
||||
<div class="gf-form">
|
||||
<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>
|
||||
`
|
||||
|
||||
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)
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType)
|
||||
|
||||
return &DingDingNotifier{
|
||||
NotifierBase: NewNotifierBase(model),
|
||||
MsgType: msgType,
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.dingding"),
|
||||
}, nil
|
||||
@ -40,8 +54,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
|
||||
type DingDingNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
log log.Logger
|
||||
MsgType string
|
||||
Url string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
message := evalContext.Rule.Message
|
||||
@ -61,15 +86,39 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
message = title
|
||||
}
|
||||
|
||||
bodyJSON, err := simplejson.NewJson([]byte(`{
|
||||
"msgtype": "link",
|
||||
"link": {
|
||||
"text": "` + message + `",
|
||||
"title": "` + title + `",
|
||||
"picUrl": "` + picUrl + `",
|
||||
"messageUrl": "` + messageUrl + `"
|
||||
for i, match := range evalContext.EvalMatches {
|
||||
message += fmt.Sprintf("\\n%2d. %s: %s", i+1, match.Metric, match.Value)
|
||||
}
|
||||
|
||||
var bodyStr string
|
||||
if this.MsgType == "actionCard" {
|
||||
// Embed the pic into the markdown directly because actionCard doesn't have a picUrl field
|
||||
if picUrl != "" {
|
||||
message = "\\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 {
|
||||
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
|
||||
|
@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if result == nil || len(result) == 0 || result[0] == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxId = toInt64(result[0]["id"])
|
||||
|
||||
if maxId == 0 {
|
||||
|
15
public/app/core/specs/kbn.test.ts
Normal file
15
public/app/core/specs/kbn.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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 => {
|
||||
}
|
||||
|
||||
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]);
|
||||
};
|
||||
|
||||
|
@ -16,18 +16,20 @@ const template = `
|
||||
<form name="ctrl.saveForm" class="modal-content" novalidate>
|
||||
<div class="p-t-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<folder-picker initial-folder-id="ctrl.folderId"
|
||||
<folder-picker initial-folder-id="ctrl.folderId"
|
||||
on-change="ctrl.onFolderChange($folder)"
|
||||
enter-folder-creation="ctrl.onEnterFolderCreation()"
|
||||
exit-folder-creation="ctrl.onExitFolderCreation()"
|
||||
enable-create-new="true"
|
||||
label-class="width-7"
|
||||
label-class="width-8"
|
||||
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>
|
||||
|
||||
@ -44,6 +46,7 @@ export class SaveDashboardAsModalCtrl {
|
||||
folderId: any;
|
||||
dismiss: () => void;
|
||||
isValidFolderSelection = true;
|
||||
copyTags: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv) {
|
||||
@ -55,6 +58,7 @@ export class SaveDashboardAsModalCtrl {
|
||||
this.clone.editable = true;
|
||||
this.clone.hideControls = false;
|
||||
this.folderId = dashboard.meta.folderId;
|
||||
this.copyTags = false;
|
||||
|
||||
// remove alerts if source dashboard is already persisted
|
||||
// do not want to create alert dupes
|
||||
@ -71,6 +75,10 @@ export class SaveDashboardAsModalCtrl {
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.copyTags) {
|
||||
this.clone.tags = [];
|
||||
}
|
||||
|
||||
return this.dashboardSrv.save(this.clone, { folderId: this.folderId }).then(this.dismiss);
|
||||
}
|
||||
|
||||
|
90
public/app/plugins/panel/text2/TextPanel.tsx
Normal file
90
public/app/plugins/panel/text2/TextPanel.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
// 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, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.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 }} />;
|
||||
}
|
||||
}
|
38
public/app/plugins/panel/text2/TextPanelEditor.tsx
Normal file
38
public/app/plugins/panel/text2/TextPanelEditor.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
|
||||
export class Text2 extends PureComponent<PanelProps> {
|
||||
constructor(props: PanelProps) {
|
||||
super(props);
|
||||
}
|
||||
import { TextPanelEditor } from './TextPanelEditor';
|
||||
import { TextPanel } from './TextPanel';
|
||||
import { TextOptions, defaults } from './types';
|
||||
|
||||
render() {
|
||||
return <h2>Text Panel!</h2>;
|
||||
}
|
||||
}
|
||||
export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel);
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin(Text2);
|
||||
reactPanel.setEditor(TextPanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
|
14
public/app/plugins/panel/text2/types.ts
Normal file
14
public/app/plugins/panel/text2/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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 @@
|
||||
border-radius: 3px;
|
||||
text-shadow: none;
|
||||
font-size: 13px;
|
||||
padding: 3px 6px 1px 6px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
box-shadow: 0 0 1px rgba($white, 0.2);
|
||||
padding: 2px 6px 2px 6px;
|
||||
border: 1px solid lighten($purple, 10%);
|
||||
|
||||
.icon-tag {
|
||||
position: relative;
|
||||
|
@ -33,10 +33,12 @@ program
|
||||
.description('Prepares @grafana/ui release (and publishes to npm on demand)')
|
||||
.option('-p, --publish', 'Publish @grafana/ui to npm registry')
|
||||
.option('-u, --usePackageJsonVersion', 'Use version specified in package.json')
|
||||
.option('--createVersionCommit', 'Create and push version commit')
|
||||
.action(async cmd => {
|
||||
await execTask(releaseTask)({
|
||||
publishToNpm: !!cmd.publish,
|
||||
usePackageJsonVersion: !!cmd.usePackageJsonVersion,
|
||||
createVersionCommit: !!cmd.createVersionCommit,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -9,10 +9,10 @@ import { savePackage, buildTask } from './grafanaui.build';
|
||||
import { TaskRunner, Task } from './task';
|
||||
|
||||
type VersionBumpType = 'prerelease' | 'patch' | 'minor' | 'major';
|
||||
|
||||
interface ReleaseTaskOptions {
|
||||
publishToNpm: boolean;
|
||||
usePackageJsonVersion: boolean;
|
||||
createVersionCommit: boolean;
|
||||
}
|
||||
|
||||
const promptBumpType = async () => {
|
||||
@ -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) =>
|
||||
useSpinner<void>(`Saving version ${version} to package.json`, async () => {
|
||||
changeCwdToGrafanaUi();
|
||||
@ -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) {
|
||||
// TODO: Ensure release branch
|
||||
// When need to update this when we star keeping @grafana/ui releases in sync with core
|
||||
await ensureMasterBranch();
|
||||
}
|
||||
|
||||
@ -145,10 +164,15 @@ const releaseTaskRunner: TaskRunner<ReleaseTaskOptions> = async ({ publishToNpm,
|
||||
await bumpVersion(nextVersion);
|
||||
}
|
||||
|
||||
if (createVersionCommit) {
|
||||
await prepareVersionCommitAndPush(nextVersion);
|
||||
}
|
||||
|
||||
if (publishToNpm) {
|
||||
await publishPackage(pkg.name, nextVersion);
|
||||
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();
|
||||
} else {
|
||||
console.log(
|
||||
|
Loading…
Reference in New Issue
Block a user