diff --git a/.dockerignore b/.dockerignore index e50dfd86aa3..c535fa427b5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,9 +3,12 @@ .git .gitignore .github +.vscode +bin data* dist docker +Dockerfile docs dump.rdb node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 4983dbafdcd..198b28ca392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) +* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) ### Breaking changes diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..f7e45893c38 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,82 @@ +# Golang build container +FROM golang:1.10 + +WORKDIR $GOPATH/src/github.com/grafana/grafana + +COPY Gopkg.toml Gopkg.lock ./ +COPY vendor vendor + +ARG DEP_ENSURE="" +RUN if [ ! -z "${DEP_ENSURE}" ]; then \ + go get -u github.com/golang/dep/cmd/dep && \ + dep ensure --vendor-only; \ + fi + +COPY pkg pkg +COPY build.go build.go +COPY package.json package.json + +RUN go run build.go build + +# Node build container +FROM node:8 + +WORKDIR /usr/src/app/ + +COPY package.json yarn.lock ./ +RUN yarn install --pure-lockfile --no-progress + +COPY Gruntfile.js tsconfig.json tslint.json ./ +COPY public public +COPY scripts scripts +COPY emails emails + +ENV NODE_ENV production +RUN ./node_modules/.bin/grunt build + +# Final container +FROM debian:stretch-slim + +ARG GF_UID="472" +ARG GF_GID="472" + +ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \ + GF_PATHS_DATA="/var/lib/grafana" \ + GF_PATHS_HOME="/usr/share/grafana" \ + GF_PATHS_LOGS="/var/log/grafana" \ + GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ + GF_PATHS_PROVISIONING="/etc/grafana/provisioning" + +WORKDIR $GF_PATHS_HOME + +RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +COPY conf ./conf + +RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ + groupadd -r -g $GF_GID grafana && \ + useradd -r -u $GF_UID -g grafana grafana && \ + mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ + "$GF_PATHS_PROVISIONING/dashboards" \ + "$GF_PATHS_LOGS" \ + "$GF_PATHS_PLUGINS" \ + "$GF_PATHS_DATA" && \ + cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \ + cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \ + chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \ + chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" + +COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/ +COPY --from=1 /usr/src/app/public ./public +COPY --from=1 /usr/src/app/tools ./tools +COPY tools/phantomjs/render.js ./tools/phantomjs/render.js + +EXPOSE 3000 + +COPY ./packaging/docker/run.sh /run.sh + +USER grafana +ENTRYPOINT [ "/run.sh" ] diff --git a/Makefile b/Makefile index 9e136688eb7..c6915409ed7 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,9 @@ build-docker-dev: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker cd packaging/docker && docker build --tag grafana/grafana:dev . +build-docker-full: + docker build --tag grafana/grafana:dev . + test-go: go test -v ./pkg/... diff --git a/NOTICE.md b/NOTICE.md index ca148971b62..899b2a3c3f9 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,5 +1,5 @@ -Copyright 2014-2017 Grafana Labs +Copyright 2014-2018 Grafana Labs This software is based on Kibana: Copyright 2012-2013 Elasticsearch BV diff --git a/README.md b/README.md index b2baf0ece59..d6083bb1504 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv ```bash npm install -g yarn yarn install --pure-lockfile -npm run watch +yarn run watch ``` Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333): @@ -54,14 +54,14 @@ env GRAFANA_THEME=light yarn start ``` Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload. -Run tests +Run tests ```bash -npm run jest +yarn run jest ``` Run karma tests ```bash -npm run karma +yarn run karma ``` ### Recompile backend on source change @@ -98,30 +98,42 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = #### Frontend Execute all frontend tests ```bash -npm run test +yarn run test ``` Writing & watching frontend tests (we have two test runners) - jest for all new tests that do not require browser context (React+more) - - Start watcher: `npm run jest` + - Start watcher: `yarn run jest` - Jest will run all test files that end with the name ".jest.ts" - karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). - - Start watcher: `npm run karma` + - Start watcher: `yarn run karma` - Karma+Mocha runs all files that end with the name "_specs.ts". #### Backend ```bash # Run Golang tests using sqlite3 as database (default) -go test ./pkg/... +go test ./pkg/... # Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests -GRAFANA_TEST_DB=mysql go test ./pkg/... +GRAFANA_TEST_DB=mysql go test ./pkg/... # Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests -GRAFANA_TEST_DB=postgres go test ./pkg/... +GRAFANA_TEST_DB=postgres go test ./pkg/... ``` +## Building custom docker image + +You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself. +```bash +git clone https://github.com/grafana/grafana +cd grafana +docker build -t grafana:dev . +docker run -d --name=grafana -p 3000:3000 grafana:dev +``` + +Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). + ## Contribute If you have any idea for an improvement or found a bug, do not hesitate to open an issue. diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 9a381b9e467..f01bf717b34 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -48,6 +48,7 @@ bind_dn = "cn=admin,dc=grafana,dc=org" bind_password = 'grafana' # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" +# Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))" search_filter = "(cn=%s)" # An array of base dns to search through diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index e2109b74909..890d6a4fb11 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -23,6 +23,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ GF_PATHS_PROVISIONING="/etc/grafana/provisioning" +WORKDIR $GF_PATHS_HOME + RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* @@ -47,5 +49,4 @@ EXPOSE 3000 COPY ./run.sh /run.sh USER grafana -WORKDIR / ENTRYPOINT [ "/run.sh" ] diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 3ee5bceae8b..9620ac4f91b 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -19,6 +19,16 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; const MAX_HISTORY_ITEMS = 100; +function makeHints(hints) { + const hintsByIndex = []; + hints.forEach(hint => { + if (hint) { + hintsByIndex[hint.index] = hint; + } + }); + return hintsByIndex; +} + function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; @@ -37,7 +47,7 @@ function makeTimeSeriesList(dataList, options) { }); } -function parseInitialState(initial: string | undefined) { +function parseUrlState(initial: string | undefined) { if (initial) { try { const parsed = JSON.parse(decodePathComponent(initial)); @@ -64,8 +74,9 @@ interface IExploreState { latency: number; loading: any; logsResult: any; - queries: any; - queryError: any; + queries: any[]; + queryErrors: any[]; + queryHints: any[]; range: any; requestOptions: any; showingGraph: boolean; @@ -82,7 +93,8 @@ export class Explore extends React.Component { constructor(props) { super(props); - const { datasource, queries, range } = parseInitialState(props.routeParams.state); + const initialState: IExploreState = props.initialState; + const { datasource, queries, range } = parseUrlState(props.routeParams.state); this.state = { datasource: null, datasourceError: null, @@ -95,7 +107,8 @@ export class Explore extends React.Component { loading: false, logsResult: null, queries: ensureQueries(queries), - queryError: null, + queryErrors: [], + queryHints: [], range: range || { ...DEFAULT_RANGE }, requestOptions: null, showingGraph: true, @@ -105,7 +118,7 @@ export class Explore extends React.Component { supportsLogs: null, supportsTable: null, tableResult: null, - ...props.initialState, + ...initialState, }; } @@ -156,6 +169,10 @@ export class Explore extends React.Component { const historyKey = `grafana.explore.history.${datasourceId}`; const history = store.getObject(historyKey, []); + if (datasource.init) { + datasource.init(); + } + this.setState( { datasource, @@ -191,6 +208,8 @@ export class Explore extends React.Component { datasourceLoading: true, graphResult: null, logsResult: null, + queryErrors: [], + queryHints: [], tableResult: null, }); const datasource = await this.props.datasourceSrv.get(option.value); @@ -199,6 +218,7 @@ export class Explore extends React.Component { onChangeQuery = (value: string, index: number, override?: boolean) => { const { queries } = this.state; + let { queryErrors, queryHints } = this.state; const prevQuery = queries[index]; const edited = override ? false : prevQuery.query !== value; const nextQuery = { @@ -208,7 +228,18 @@ export class Explore extends React.Component { }; const nextQueries = [...queries]; nextQueries[index] = nextQuery; - this.setState({ queries: nextQueries }, override ? () => this.onSubmit() : undefined); + if (override) { + queryErrors = []; + queryHints = []; + } + this.setState( + { + queryErrors, + queryHints, + queries: nextQueries, + }, + override ? () => this.onSubmit() : undefined + ); }; onChangeTime = nextRange => { @@ -255,13 +286,32 @@ export class Explore extends React.Component { }; onClickTableCell = (columnKey: string, rowValue: string) => { + this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue }); + }; + + onModifyQueries = (action: object, index?: number) => { const { datasource, queries } = this.state; if (datasource && datasource.modifyQuery) { - const nextQueries = queries.map(q => ({ - ...q, - edited: false, - query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }), - })); + let nextQueries; + if (index === undefined) { + // Modify all queries + nextQueries = queries.map(q => ({ + ...q, + edited: false, + query: datasource.modifyQuery(q.query, action), + })); + } else { + // Modify query only at index + nextQueries = [ + ...queries.slice(0, index), + { + ...queries[index], + edited: false, + query: datasource.modifyQuery(queries[index].query, action), + }, + ...queries.slice(index + 1), + ]; + } this.setState({ queries: nextQueries }, () => this.onSubmit()); } }; @@ -309,7 +359,7 @@ export class Explore extends React.Component { this.setState({ history }); } - buildQueryOptions(targetOptions: { format: string; instant?: boolean }) { + buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, queries, range } = this.state; const resolution = this.el.offsetWidth; const absoluteRange = { @@ -333,19 +383,20 @@ export class Explore extends React.Component { if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, graphResult: null, queryError: null }); + this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] }); const now = Date.now(); - const options = this.buildQueryOptions({ format: 'time_series', instant: false }); + const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true }); try { const res = await datasource.query(options); const result = makeTimeSeriesList(res.data, options); + const queryHints = res.hints ? makeHints(res.hints) : []; const latency = Date.now() - now; - this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); + this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options }); this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -354,7 +405,7 @@ export class Explore extends React.Component { if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryError: null, tableResult: null }); + this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null }); const now = Date.now(); const options = this.buildQueryOptions({ format: 'table', @@ -369,7 +420,7 @@ export class Explore extends React.Component { } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -378,7 +429,7 @@ export class Explore extends React.Component { if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryError: null, logsResult: null }); + this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null }); const now = Date.now(); const options = this.buildQueryOptions({ format: 'logs', @@ -393,7 +444,7 @@ export class Explore extends React.Component { } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -415,7 +466,8 @@ export class Explore extends React.Component { loading, logsResult, queries, - queryError, + queryErrors, + queryHints, range, requestOptions, showingGraph, @@ -449,12 +501,12 @@ export class Explore extends React.Component { ) : ( -
- -
- )} + + )} {!datasourceMissing ? (