From af1f3dd77b4becc34354c4395ebcf0023c2889a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 19 Dec 2017 17:26:51 +0100 Subject: [PATCH 001/117] poc: mobx poc --- package.json | 3 + public/app/containers/ServerStats.tsx | 27 +++++ public/app/core/angular_wrappers.ts | 20 ++-- public/app/features/admin/partials/stats.html | 109 +++++++++--------- public/app/store/store.ts | 16 +++ 5 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 public/app/containers/ServerStats.tsx create mode 100644 public/app/store/store.ts diff --git a/package.json b/package.json index 17817d60ef8..32846b2d493 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,9 @@ "file-saver": "^1.3.3", "jquery": "^3.2.1", "lodash": "^4.17.4", + "mobx": "^3.4.1", + "mobx-react": "^4.3.5", + "mobx-state-tree": "^1.3.1", "moment": "^2.18.1", "mousetrap": "^1.6.0", "perfect-scrollbar": "^1.2.0", diff --git a/public/app/containers/ServerStats.tsx b/public/app/containers/ServerStats.tsx new file mode 100644 index 00000000000..8f6a6ac3ca2 --- /dev/null +++ b/public/app/containers/ServerStats.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import PageHeader from "app/core/components/PageHeader/PageHeader"; +import { NavModel, NavModelSrv } from "app/core/nav_model_srv"; + +export interface IState { + navModel: NavModel; +} + +export default class ServerStats extends React.Component { + constructor(props) { + super(props); + + const navModelSrv = new NavModelSrv(); + + this.state = { + navModel: navModelSrv.getNav("cfg", "admin", "server-stats", 1) + }; + } + + render() { + return ( + +

ServerStats

+
+ ); + } +} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 83a70fa4c8a..2774b46d3f4 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,12 +1,14 @@ -import { react2AngularDirective } from 'app/core/utils/react2angular'; -import { PasswordStrength } from './components/PasswordStrength'; -import PageHeader from './components/PageHeader/PageHeader'; -import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; -import LoginBackground from './components/Login/LoginBackground'; +import { react2AngularDirective } from "app/core/utils/react2angular"; +import { PasswordStrength } from "./components/PasswordStrength"; +import PageHeader from "./components/PageHeader/PageHeader"; +import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA"; +import LoginBackground from "./components/Login/LoginBackground"; +import ServerStats from "app/containers/ServerStats"; export function registerAngularDirectives() { - react2AngularDirective('passwordStrength', PasswordStrength, ['password']); - react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); - react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); - react2AngularDirective('loginBackground', LoginBackground, []); + react2AngularDirective("passwordStrength", PasswordStrength, ["password"]); + react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]); + react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]); + react2AngularDirective("loginBackground", LoginBackground, []); + react2AngularDirective("containerServerStats", ServerStats, []); } diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html index e2a4bb62301..4298046f390 100644 --- a/public/app/features/admin/partials/stats.html +++ b/public/app/features/admin/partials/stats.html @@ -1,54 +1,55 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameValue
Total dashboards{{ctrl.stats.dashboards}}
Total users{{ctrl.stats.users}}
Active users (seen last 14 days){{ctrl.stats.activeUsers}}
Total organizations{{ctrl.stats.orgs}}
Total datasources{{ctrl.stats.datasources}}
Total playlists{{ctrl.stats.playlists}}
Total snapshots{{ctrl.stats.snapshots}}
Total dashboard tags{{ctrl.stats.tags}}
Total starred dashboards{{ctrl.stats.stars}}
Total alerts{{ctrl.stats.alerts}}
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/store/store.ts b/public/app/store/store.ts new file mode 100644 index 00000000000..e18e73690b8 --- /dev/null +++ b/public/app/store/store.ts @@ -0,0 +1,16 @@ +import { types } from "mobx-state-tree"; + +const Search = types.model({ + name: "", + done: false +}); + +const RootStore = types.model({ + search: types.map(Search) +}); + +const store = RootStore.create({ + search: {} +}); + +export { store }; From ee216ba6fb45dbf2a5ef402e691659b11e0e7e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 20 Dec 2017 10:53:16 +0100 Subject: [PATCH 002/117] poc: mobx test --- public/app/containers/ServerStats.tsx | 25 ++++++++++++++++++++----- public/app/store/store.ts | 20 ++++++++++++-------- tsconfig.json | 2 +- yarn.lock | 18 ++++++++++++++++++ 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/public/app/containers/ServerStats.tsx b/public/app/containers/ServerStats.tsx index 8f6a6ac3ca2..b6488725f6a 100644 --- a/public/app/containers/ServerStats.tsx +++ b/public/app/containers/ServerStats.tsx @@ -1,27 +1,42 @@ import React from "react"; +import { observer } from "mobx-react"; import PageHeader from "app/core/components/PageHeader/PageHeader"; import { NavModel, NavModelSrv } from "app/core/nav_model_srv"; +import { store } from "app/store/store"; export interface IState { navModel: NavModel; + search: any; } -export default class ServerStats extends React.Component { +@observer +export default class ServerStats extends React.Component { constructor(props) { super(props); const navModelSrv = new NavModelSrv(); this.state = { - navModel: navModelSrv.getNav("cfg", "admin", "server-stats", 1) + navModel: navModelSrv.getNav("cfg", "admin", "server-stats", 1), + search: store.search }; } + onClick = () => { + this.state.search.search(); + }; + render() { + console.log("render"); return ( - -

ServerStats

-
+
+ + +
+ name: +

{this.state.search.name}

+
+
); } } diff --git a/public/app/store/store.ts b/public/app/store/store.ts index e18e73690b8..e996d804710 100644 --- a/public/app/store/store.ts +++ b/public/app/store/store.ts @@ -1,16 +1,20 @@ import { types } from "mobx-state-tree"; -const Search = types.model({ - name: "", - done: false -}); +const Search = types + .model({ + name: "asdas", + done: false + }) + .actions(self => ({ + search() { + self.name = "changed"; + } + })); const RootStore = types.model({ - search: types.map(Search) + search: types.optional(Search, {}) }); -const store = RootStore.create({ - search: {} -}); +const store = RootStore.create({}); export { store }; diff --git a/tsconfig.json b/tsconfig.json index bc9222ac87d..3596930a62f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "sourceMap": true, "noEmitOnError": false, "emitDecoratorMetadata": false, - "experimentalDecorators": false, + "experimentalDecorators": true, "noImplicitReturns": true, "noImplicitThis": false, "noImplicitUseStrict":false, diff --git a/yarn.lock b/yarn.lock index a419e1020c9..161927c0db8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4399,6 +4399,10 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" +hoist-non-react-statics@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -6303,6 +6307,20 @@ mkdirp@0.5.1, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" +mobx-react@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.3.5.tgz#76853f2f2ef4a6f960c374bcd9f01e875929c04c" + dependencies: + hoist-non-react-statics "^2.3.1" + +mobx-state-tree@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-1.3.1.tgz#9e1ba9b8b6ea183f1a4a2ae1f67bfa8f2bcae4fe" + +mobx@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.4.1.tgz#37abe5ee882d401828d9f26c6c1a2f47614bbbef" + mocha@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" From 354913a704dc8723d456df2c2476bbb3397fe167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 20 Dec 2017 17:24:04 +0100 Subject: [PATCH 003/117] mobx: progress on poc --- public/app/app.ts | 3 + public/app/containers/ServerStats.tsx | 42 ------- public/app/core/angular_wrappers.ts | 4 +- .../core/components/search/SearchResult.tsx | 84 ++++++++++++++ public/app/core/components/search/search.html | 11 +- public/app/features/admin/partials/stats.html | 109 +++++++++--------- public/app/store/store.ts | 20 ---- public/app/stores/RootStore.ts | 11 ++ public/app/stores/SearchStore.ts | 44 +++++++ public/app/stores/store.ts | 7 ++ 10 files changed, 211 insertions(+), 124 deletions(-) delete mode 100644 public/app/containers/ServerStats.tsx create mode 100644 public/app/core/components/search/SearchResult.tsx delete mode 100644 public/app/store/store.ts create mode 100644 public/app/stores/RootStore.ts create mode 100644 public/app/stores/SearchStore.ts create mode 100644 public/app/stores/store.ts diff --git a/public/app/app.ts b/public/app/app.ts index 8ddacc4ed04..0fd00c8e1bb 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -19,6 +19,7 @@ import angular from "angular"; import config from "app/core/config"; import _ from "lodash"; import moment from "moment"; +import { createStore } from "app/stores/store"; // add move to lodash for backward compatabiltiy _.move = function(array, fromIndex, toIndex) { @@ -135,6 +136,8 @@ export class GrafanaApp { Promise.all(preBootRequires) .then(() => { + createStore(); + // disable tool tip animation $.fn.tooltip.defaults.animation = false; // bootstrap the app diff --git a/public/app/containers/ServerStats.tsx b/public/app/containers/ServerStats.tsx deleted file mode 100644 index b6488725f6a..00000000000 --- a/public/app/containers/ServerStats.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react"; -import PageHeader from "app/core/components/PageHeader/PageHeader"; -import { NavModel, NavModelSrv } from "app/core/nav_model_srv"; -import { store } from "app/store/store"; - -export interface IState { - navModel: NavModel; - search: any; -} - -@observer -export default class ServerStats extends React.Component { - constructor(props) { - super(props); - - const navModelSrv = new NavModelSrv(); - - this.state = { - navModel: navModelSrv.getNav("cfg", "admin", "server-stats", 1), - search: store.search - }; - } - - onClick = () => { - this.state.search.search(); - }; - - render() { - console.log("render"); - return ( -
- - -
- name: -

{this.state.search.name}

-
-
- ); - } -} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 2774b46d3f4..839ff011d5a 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -3,12 +3,12 @@ import { PasswordStrength } from "./components/PasswordStrength"; import PageHeader from "./components/PageHeader/PageHeader"; import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA"; import LoginBackground from "./components/Login/LoginBackground"; -import ServerStats from "app/containers/ServerStats"; +import { SearchResult } from "./components/search/SearchResult"; export function registerAngularDirectives() { react2AngularDirective("passwordStrength", PasswordStrength, ["password"]); react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]); react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]); react2AngularDirective("loginBackground", LoginBackground, []); - react2AngularDirective("containerServerStats", ServerStats, []); + react2AngularDirective("searchResult", SearchResult, []); } diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx new file mode 100644 index 00000000000..fd96e55f912 --- /dev/null +++ b/public/app/core/components/search/SearchResult.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import classNames from "classnames"; +import { observer } from "mobx-react"; +import { store } from "app/stores/store"; + +export interface SearchResultProps { + search: any; +} + +@observer +export class SearchResult extends React.Component { + constructor(props) { + super(props); + + this.state = { + search: store.search + }; + } + + render() { + return this.state.search.sections.map(section => { + return ; + }); + } +} + +export interface SectionProps { + section: any; +} + +@observer +export class SearchResultSection extends React.Component { + constructor(props) { + super(props); + } + + renderItem(item) { + return ( + + + + + +
{item.title}
+
+
+ ); + } + + toggleSection = () => { + this.props.section.toggle(); + }; + + render() { + let collapseClassNames = classNames({ + fa: true, + "fa-plus": !this.props.section.expanded, + "fa-minus": this.props.section.expanded, + "search-section__header__toggle": true + }); + + return ( +
+
+ + + {this.props.section.title} + + +
+ {this.props.section.expanded && ( +
+ {this.props.section.items.map(this.renderItem)} +
+ )} +
+ ); + } +} diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 3b83284757e..50568d4ed85 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -22,11 +22,12 @@
No dashboards matching your query were found.
- + + + + + +
diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html index 4298046f390..e2a4bb62301 100644 --- a/public/app/features/admin/partials/stats.html +++ b/public/app/features/admin/partials/stats.html @@ -1,55 +1,54 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameValue
Total dashboards{{ctrl.stats.dashboards}}
Total users{{ctrl.stats.users}}
Active users (seen last 14 days){{ctrl.stats.activeUsers}}
Total organizations{{ctrl.stats.orgs}}
Total datasources{{ctrl.stats.datasources}}
Total playlists{{ctrl.stats.playlists}}
Total snapshots{{ctrl.stats.snapshots}}
Total dashboard tags{{ctrl.stats.tags}}
Total starred dashboards{{ctrl.stats.stars}}
Total alerts{{ctrl.stats.alerts}}
+
diff --git a/public/app/store/store.ts b/public/app/store/store.ts deleted file mode 100644 index e996d804710..00000000000 --- a/public/app/store/store.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { types } from "mobx-state-tree"; - -const Search = types - .model({ - name: "asdas", - done: false - }) - .actions(self => ({ - search() { - self.name = "changed"; - } - })); - -const RootStore = types.model({ - search: types.optional(Search, {}) -}); - -const store = RootStore.create({}); - -export { store }; diff --git a/public/app/stores/RootStore.ts b/public/app/stores/RootStore.ts new file mode 100644 index 00000000000..512ec8da622 --- /dev/null +++ b/public/app/stores/RootStore.ts @@ -0,0 +1,11 @@ +import { types } from "mobx-state-tree"; +import { SearchStore } from "./SearchStore"; + +export const RootStore = types.model({ + search: types.optional(SearchStore, { + sections: [] + }) +}); + +type IRootStoreType = typeof RootStore.Type; +export interface IRootStore extends IRootStoreType {} diff --git a/public/app/stores/SearchStore.ts b/public/app/stores/SearchStore.ts new file mode 100644 index 00000000000..f4f9a4ece9e --- /dev/null +++ b/public/app/stores/SearchStore.ts @@ -0,0 +1,44 @@ +import { types } from "mobx-state-tree"; + +export const ResultItem = types.model("ResultItem", { + id: types.identifier(types.number), + folderId: types.optional(types.number, 0), + title: types.string, + url: types.string, + icon: types.string, + folderTitle: types.optional(types.string, "") +}); + +export const SearchResultSection = types + .model("SearchResultSection", { + id: types.identifier(), + title: types.string, + icon: types.string, + expanded: types.boolean, + items: types.array(ResultItem) + }) + .actions(self => ({ + toggle() { + self.expanded = !self.expanded; + } + })); + +export const SearchStore = types + .model("SearchStore", { + sections: types.array(SearchResultSection) + }) + .actions(self => ({ + query() { + for (let i = 0; i < 100; i++) { + self.sections.push( + SearchResultSection.create({ + id: "starred" + i, + title: "starred", + icon: "fa fa-fw fa-star-o", + expanded: false, + items: [] + }) + ); + } + } + })); diff --git a/public/app/stores/store.ts b/public/app/stores/store.ts new file mode 100644 index 00000000000..80cb51c3006 --- /dev/null +++ b/public/app/stores/store.ts @@ -0,0 +1,7 @@ +import { RootStore, IRootStore } from "./RootStore"; + +export let store: IRootStore; + +export function createStore() { + store = RootStore.create({}); +} From d41ce4f9ca7c48a27bc7e2876e5c67f2f8bb6164 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 20 Dec 2017 21:17:55 +0100 Subject: [PATCH 004/117] Various dashboard folders improvements (#10309) * dashfolders: fix url for create dashboard from manage dashboards Don't include folderId querystring if you don't manage a folder and creating a new dashboard from there. * dashfolders: don't store folderId in dashboard model Use folderId from meta instead #10307 * dashfolders: minor ux fix * dashfolders: minor fix for edit folder title --- .../manage_dashboards/manage_dashboards.html | 8 +++----- .../manage_dashboards/manage_dashboards.ts | 10 ++++++++++ public/app/core/routes/dashboard_loaders.ts | 8 ++++++-- public/app/core/services/backend_srv.ts | 14 ++++++++------ public/app/features/dashboard/dashboard_model.ts | 2 -- public/app/features/dashboard/dashboard_srv.ts | 3 +++ .../features/dashboard/folder_settings_ctrl.ts | 16 ++++++++++++++++ .../dashboard/partials/folder_settings.html | 4 ++-- public/app/features/dashboard/save_as_modal.ts | 2 +- .../app/features/dashboard/settings/settings.ts | 1 - 10 files changed, 49 insertions(+), 19 deletions(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 90091f3032c..cb2cec28bab 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -5,7 +5,7 @@
- + Dashboard @@ -60,22 +60,20 @@ switch-class="gf-form-switch--transparent gf-form-switch--search-result-filter-row__checkbox" />
-
+
diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index b8132f6f643..a985a9fee7b 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -297,6 +297,16 @@ export class ManageDashboardsCtrl { this.query.starred = false; this.getDashboards(); } + + createDashboardUrl() { + let url = '/dashboard/new'; + + if (this.folderId) { + url += `?folderId=${this.folderId}`; + } + + return url; + } } export function manageDashboardsDirective() { diff --git a/public/app/core/routes/dashboard_loaders.ts b/public/app/core/routes/dashboard_loaders.ts index 7b6ed068b30..937c66bb853 100644 --- a/public/app/core/routes/dashboard_loaders.ts +++ b/public/app/core/routes/dashboard_loaders.ts @@ -34,7 +34,12 @@ export class NewDashboardCtrl { constructor($scope, $routeParams) { $scope.initDashboard( { - meta: { canStar: false, canShare: false, isNew: true }, + meta: { + canStar: false, + canShare: false, + isNew: true, + folderId: Number($routeParams.folderId), + }, dashboard: { title: 'New dashboard', panels: [ @@ -44,7 +49,6 @@ export class NewDashboardCtrl { title: 'Panel Title', }, ], - folderId: Number($routeParams.folderId), }, }, $scope diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 01ca5a70d70..30b9da700e3 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -242,7 +242,7 @@ export class BackendSrv { return this.post('/api/dashboards/db/', { dashboard: dash, - folderId: dash.folderId, + folderId: options.folderId, overwrite: options.overwrite === true, message: options.message || '', }); @@ -324,20 +324,22 @@ export class BackendSrv { return; } - model.folderId = toFolder.id; - model.meta.folderId = toFolder.id; - model.meta.folderTitle = toFolder.title; const clone = model.getSaveModelClone(); + let options = { + folderId: toFolder.id, + overwrite: false, + }; - this.saveDashboard(clone, {}) + this.saveDashboard(clone, options) .then(() => { deferred.resolve({ succeeded: true }); }) .catch(err => { if (err.data && err.data.status === 'plugin-dashboard') { err.isHandled = true; + options.overwrite = true; - this.saveDashboard(clone, { overwrite: true }) + this.saveDashboard(clone, options) .then(() => { deferred.resolve({ succeeded: true }); }) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 5def153f8fb..2f1c3cba447 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -31,7 +31,6 @@ export class DashboardModel { revision: number; links: any; gnetId: any; - folderId: number; panels: PanelModel[]; // ------------------ @@ -76,7 +75,6 @@ export class DashboardModel { this.version = data.version || 0; this.links = data.links || []; this.gnetId = data.gnetId || null; - this.folderId = data.folderId || null; this.panels = _.map( data.panels || [], panelData => new PanelModel(panelData) diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index b3c3b5e40dc..fddc0422f85 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -84,6 +84,9 @@ export class DashboardSrv { } save(clone, options) { + options = options || {}; + options.folderId = this.dash.meta.folderId; + return this.backendSrv .saveDashboard(clone, options) .then(this.postSave.bind(this, clone)) diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts index 29da2666534..5f0d805192b 100644 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ b/public/app/features/dashboard/folder_settings_ctrl.ts @@ -8,6 +8,8 @@ export class FolderSettingsCtrl { canSave = false; dashboard: any; meta: any; + title: string; + hasChanged: boolean; /** @ngInject */ constructor( @@ -29,11 +31,20 @@ export class FolderSettingsCtrl { this.dashboard = result.dashboard; this.meta = result.meta; this.canSave = result.meta.canSave; + this.title = this.dashboard.title; }); } } save() { + this.titleChanged(); + + if (!this.hasChanged) { + return; + } + + this.dashboard.title = this.title.trim(); + return this.backendSrv .saveDashboard(this.dashboard, { overwrite: false }) .then(result => { @@ -52,6 +63,11 @@ export class FolderSettingsCtrl { .catch(this.handleSaveFolderError); } + titleChanged() { + this.hasChanged = + this.dashboard.title.toLowerCase() !== this.title.trim().toLowerCase(); + } + delete(evt) { if (evt) { evt.stopPropagation(); diff --git a/public/app/features/dashboard/partials/folder_settings.html b/public/app/features/dashboard/partials/folder_settings.html index 37fdfef4a3f..6c53c330d5d 100644 --- a/public/app/features/dashboard/partials/folder_settings.html +++ b/public/app/features/dashboard/partials/folder_settings.html @@ -7,10 +7,10 @@
- +
- diff --git a/public/app/features/dashboard/save_as_modal.ts b/public/app/features/dashboard/save_as_modal.ts index 84679b90ff8..236b1f07b56 100644 --- a/public/app/features/dashboard/save_as_modal.ts +++ b/public/app/features/dashboard/save_as_modal.ts @@ -52,7 +52,7 @@ export class SaveDashboardAsModalCtrl { this.clone.title += ' Copy'; this.clone.editable = true; this.clone.hideControls = false; - this.folderId = dashboard.folderId; + this.folderId = dashboard.meta.folderId; // remove alerts if source dashboard is already persisted // do not want to create alert dupes diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index f109a1bdeb8..3b124326ada 100644 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -185,7 +185,6 @@ export class SettingsCtrl { } onFolderChange(folder) { - this.dashboard.folderId = folder.id; this.dashboard.meta.folderId = folder.id; this.dashboard.meta.folderTitle = folder.title; } From af34f9977ed894fbe9abb2ce02ebff872f194b3c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 20 Dec 2017 21:20:12 +0100 Subject: [PATCH 005/117] Add avatar to team and team members page (#10305) * teams: add db migration for email column in teams table * teams: /teams should render index page with a 200 OK * teams: additional backend functionality for team and team members Possibility to save/update email for teams. Possibility to retrive avatar url when searching for teams. Possibility to retrive avatar url when searching for team members. * teams: display team avatar and team member avatars Possibility to save and update email for a team * teams: create team on separate page instead of modal dialog --- pkg/api/api.go | 3 ++ pkg/api/dtos/models.go | 17 +++++++ pkg/api/team.go | 5 ++ pkg/api/team_members.go | 5 ++ pkg/models/team.go | 9 +++- pkg/models/team_member.go | 11 +++-- pkg/services/sqlstore/migrations/team_mig.go | 5 ++ pkg/services/sqlstore/team.go | 5 ++ pkg/services/sqlstore/team_test.go | 6 ++- public/app/core/routes/routes.ts | 6 +++ public/app/features/org/all.ts | 26 +++++----- public/app/features/org/create_team_ctrl.ts | 26 ++++++++++ public/app/features/org/create_team_modal.ts | 36 -------------- .../features/org/partials/create_team.html | 47 +++++++++---------- .../features/org/partials/team_details.html | 23 +++++++-- public/app/features/org/partials/teams.html | 12 ++++- public/app/features/org/team_details_ctrl.ts | 6 ++- 17 files changed, 159 insertions(+), 89 deletions(-) create mode 100644 public/app/features/org/create_team_ctrl.ts delete mode 100644 public/app/features/org/create_team_modal.ts diff --git a/pkg/api/api.go b/pkg/api/api.go index 3f6d8d4d954..ea082ff4741 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -40,8 +40,11 @@ func (hs *HttpServer) registerRoutes() { r.Get("/datasources/", reqSignedIn, Index) r.Get("/datasources/new", reqSignedIn, Index) r.Get("/datasources/edit/*", reqSignedIn, Index) + r.Get("/org/users", reqSignedIn, Index) r.Get("/org/users/new", reqSignedIn, Index) r.Get("/org/users/invite", reqSignedIn, Index) + r.Get("/org/teams", reqSignedIn, Index) + r.Get("/org/teams/*", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index) r.Get("/configuration", reqGrafanaAdmin, Index) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 2e9aa78d7d5..a702b06fad5 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -3,6 +3,7 @@ package dtos import ( "crypto/md5" "fmt" + "regexp" "strings" "github.com/grafana/grafana/pkg/components/simplejson" @@ -57,3 +58,19 @@ func GetGravatarUrl(text string) string { hasher.Write([]byte(strings.ToLower(text))) return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil)) } + +func GetGravatarUrlWithDefault(text string, defaultText string) string { + if text != "" { + return GetGravatarUrl(text) + } + + reg, err := regexp.Compile("[^a-zA-Z0-9]+") + + if err != nil { + return "" + } + + text = reg.ReplaceAllString(defaultText, "") + "@localhost" + + return GetGravatarUrl(text) +} diff --git a/pkg/api/team.go b/pkg/api/team.go index 31e465d3232..af537224d41 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -70,6 +71,10 @@ func SearchTeams(c *middleware.Context) Response { return ApiError(500, "Failed to search Teams", err) } + for _, team := range query.Result.Teams { + team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + } + query.Result.Page = page query.Result.PerPage = perPage diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 0999c9573a5..412e142edb7 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -1,6 +1,7 @@ package api import ( + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" @@ -15,6 +16,10 @@ func GetTeamMembers(c *middleware.Context) Response { return ApiError(500, "Failed to get Team Members", err) } + for _, member := range query.Result { + member.AvatarUrl = dtos.GetGravatarUrl(member.Email) + } + return Json(200, query.Result) } diff --git a/pkg/models/team.go b/pkg/models/team.go index b9759f059cf..d2912f431b8 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -16,6 +16,7 @@ type Team struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` Name string `json:"name"` + Email string `json:"email"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` @@ -26,14 +27,16 @@ type Team struct { type CreateTeamCommand struct { Name string `json:"name" binding:"Required"` + Email string `json:"email"` OrgId int64 `json:"-"` Result Team `json:"-"` } type UpdateTeamCommand struct { - Id int64 - Name string + Id int64 + Name string + Email string } type DeleteTeamCommand struct { @@ -64,6 +67,8 @@ type SearchTeamDto struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` Name string `json:"name"` + Email string `json:"email"` + AvatarUrl string `json:"avatarUrl"` MemberCount int64 `json:"memberCount"` } diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 71e5cd4ba12..9970678a1ae 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -47,9 +47,10 @@ type GetTeamMembersQuery struct { // Projections and DTOs type TeamMemberDTO struct { - OrgId int64 `json:"orgId"` - TeamId int64 `json:"teamId"` - UserId int64 `json:"userId"` - Email string `json:"email"` - Login string `json:"login"` + OrgId int64 `json:"orgId"` + TeamId int64 `json:"teamId"` + UserId int64 `json:"userId"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatarUrl"` } diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index 374972e5449..eb0641fbc32 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -45,4 +45,9 @@ func addTeamMigrations(mg *Migrator) { //------- indexes ------------------ mg.AddMigration("add index team_member.org_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[0])) mg.AddMigration("add unique index team_member_org_id_team_id_user_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[1])) + + // add column email + mg.AddMigration("Add column email to team table", NewAddColumnMigration(teamV1, &Column{ + Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190, + })) } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 3e9a6e6ec56..98bb1a36eb9 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -33,6 +33,7 @@ func CreateTeam(cmd *m.CreateTeamCommand) error { team := m.Team{ Name: cmd.Name, + Email: cmd.Email, OrgId: cmd.OrgId, Created: time.Now(), Updated: time.Now(), @@ -57,9 +58,12 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error { team := m.Team{ Name: cmd.Name, + Email: cmd.Email, Updated: time.Now(), } + sess.MustCols("email") + affectedRows, err := sess.Id(cmd.Id).Update(&team) if err != nil { @@ -125,6 +129,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error { sql.WriteString(`select team.id as id, team.name as name, + team.email as email, (select count(*) from team_member where team_member.team_id = team.id) as member_count from team as team where team.org_id = ?`) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index 4a099db14ff..dbae4545266 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -27,8 +27,8 @@ func TestTeamCommandsAndQueries(t *testing.T) { userIds = append(userIds, userCmd.Result.Id) } - group1 := m.CreateTeamCommand{Name: "group1 name"} - group2 := m.CreateTeamCommand{Name: "group2 name"} + group1 := m.CreateTeamCommand{Name: "group1 name", Email: "test1@test.com"} + group2 := m.CreateTeamCommand{Name: "group2 name", Email: "test2@test.com"} err := CreateTeam(&group1) So(err, ShouldBeNil) @@ -43,6 +43,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { team1 := query.Result.Teams[0] So(team1.Name, ShouldEqual, "group1 name") + So(team1.Email, ShouldEqual, "test1@test.com") err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]}) So(err, ShouldBeNil) @@ -76,6 +77,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) So(query.Result[0].Name, ShouldEqual, "group2 name") + So(query.Result[0].Email, ShouldEqual, "test2@test.com") }) Convey("Should be able to remove users from a group", func() { diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 2e4950cac6f..b8b157b5ad0 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -145,6 +145,12 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', resolve: loadOrgBundle, }) + .when('/org/teams/new', { + templateUrl: 'public/app/features/org/partials/create_team.html', + controller: 'CreateTeamCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, + }) .when('/org/teams/edit/:id', { templateUrl: 'public/app/features/org/partials/team_details.html', controller: 'TeamDetailsCtrl', diff --git a/public/app/features/org/all.ts b/public/app/features/org/all.ts index 11a362d728a..50b81356c70 100644 --- a/public/app/features/org/all.ts +++ b/public/app/features/org/all.ts @@ -1,13 +1,13 @@ -import './org_users_ctrl'; -import './profile_ctrl'; -import './org_users_ctrl'; -import './select_org_ctrl'; -import './change_password_ctrl'; -import './new_org_ctrl'; -import './user_invite_ctrl'; -import './teams_ctrl'; -import './team_details_ctrl'; -import './create_team_modal'; -import './org_api_keys_ctrl'; -import './org_details_ctrl'; -import './prefs_control'; +import "./org_users_ctrl"; +import "./profile_ctrl"; +import "./org_users_ctrl"; +import "./select_org_ctrl"; +import "./change_password_ctrl"; +import "./new_org_ctrl"; +import "./user_invite_ctrl"; +import "./teams_ctrl"; +import "./team_details_ctrl"; +import "./create_team_ctrl"; +import "./org_api_keys_ctrl"; +import "./org_details_ctrl"; +import "./prefs_control"; diff --git a/public/app/features/org/create_team_ctrl.ts b/public/app/features/org/create_team_ctrl.ts new file mode 100644 index 00000000000..08184076a1c --- /dev/null +++ b/public/app/features/org/create_team_ctrl.ts @@ -0,0 +1,26 @@ +import coreModule from "app/core/core_module"; + +export default class CreateTeamCtrl { + name: string; + email: string; + navModel: any; + + /** @ngInject **/ + constructor(private backendSrv, private $location, navModelSrv) { + this.navModel = navModelSrv.getNav("cfg", "teams", 0); + } + + create() { + const payload = { + name: this.name, + email: this.email + }; + this.backendSrv.post("/api/teams", payload).then(result => { + if (result.teamId) { + this.$location.path("/org/teams/edit/" + result.teamId); + } + }); + } +} + +coreModule.controller("CreateTeamCtrl", CreateTeamCtrl); diff --git a/public/app/features/org/create_team_modal.ts b/public/app/features/org/create_team_modal.ts deleted file mode 100644 index acac782fadc..00000000000 --- a/public/app/features/org/create_team_modal.ts +++ /dev/null @@ -1,36 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; - -export class CreateTeamCtrl { - teamName = ''; - - /** @ngInject */ - constructor(private backendSrv, private $location) {} - - createTeam() { - this.backendSrv.post('/api/teams', { name: this.teamName }).then(result => { - if (result.teamId) { - this.$location.path('/org/teams/edit/' + result.teamId); - } - this.dismiss(); - }); - } - - dismiss() { - appEvents.emit('hide-modal'); - } -} - -export function createTeamModal() { - return { - restrict: 'E', - templateUrl: 'public/app/features/org/partials/create_team.html', - controller: CreateTeamCtrl, - bindToController: true, - controllerAs: 'ctrl', - }; -} - -coreModule.directive('createTeamModal', createTeamModal); diff --git a/public/app/features/org/partials/create_team.html b/public/app/features/org/partials/create_team.html index f45ad0b33a0..9f606408164 100644 --- a/public/app/features/org/partials/create_team.html +++ b/public/app/features/org/partials/create_team.html @@ -1,27 +1,26 @@ -