diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ecc3339600..e0fd5caf819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212) * **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon) * **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030) +* **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173) # 5.3.0 (unreleased) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index e1e131d9f3a..ee9d9583c4e 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -235,7 +235,7 @@ func parseMultiSelectValue(input string) []string { func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { regions := []string{ "ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1", - "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", + "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1", } result := make([]suggestData, 0) diff --git a/public/app/core/components/LayoutSelector/LayoutSelector.tsx b/public/app/core/components/LayoutSelector/LayoutSelector.tsx new file mode 100644 index 00000000000..d9e00102438 --- /dev/null +++ b/public/app/core/components/LayoutSelector/LayoutSelector.tsx @@ -0,0 +1,39 @@ +import React, { SFC } from 'react'; + +export type LayoutMode = LayoutModes.Grid | LayoutModes.List; + +export enum LayoutModes { + Grid = 'grid', + List = 'list', +} + +interface Props { + mode: LayoutMode; + onLayoutModeChanged: (mode: LayoutMode) => {}; +} + +const LayoutSelector: SFC = props => { + const { mode, onLayoutModeChanged } = props; + return ( +
+ + +
+ ); +}; + +export default LayoutSelector; diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index f240a86efba..d7cee33e6c3 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -4,7 +4,8 @@ - + + diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 187d68133cd..50d894de43f 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -528,10 +528,11 @@ export class Explore extends React.Component { {!datasourceMissing ? (
+ + + setLayoutMode(mode)} /> +
+ + ); + } +} + +function mapStateToProps(state) { + return { + searchQuery: getPluginsSearchQuery(state.plugins), + layoutMode: getLayoutMode(state.plugins), + }; +} + +const mapDispatchToProps = { + setPluginsSearchQuery, + setLayoutMode, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar); diff --git a/public/app/features/plugins/PluginList.test.tsx b/public/app/features/plugins/PluginList.test.tsx new file mode 100644 index 00000000000..201dd69b9db --- /dev/null +++ b/public/app/features/plugins/PluginList.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PluginList from './PluginList'; +import { getMockPlugins } from './__mocks__/pluginMocks'; +import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; + +const setup = (propOverrides?: object) => { + const props = Object.assign( + { + plugins: getMockPlugins(5), + layoutMode: LayoutModes.Grid, + }, + propOverrides + ); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx new file mode 100644 index 00000000000..0074839e754 --- /dev/null +++ b/public/app/features/plugins/PluginList.tsx @@ -0,0 +1,32 @@ +import React, { SFC } from 'react'; +import classNames from 'classnames/bind'; +import PluginListItem from './PluginListItem'; +import { Plugin } from 'app/types'; +import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; + +interface Props { + plugins: Plugin[]; + layoutMode: LayoutMode; +} + +const PluginList: SFC = props => { + const { plugins, layoutMode } = props; + + const listStyle = classNames({ + 'card-section': true, + 'card-list-layout-grid': layoutMode === LayoutModes.Grid, + 'card-list-layout-list': layoutMode === LayoutModes.List, + }); + + return ( +
+
    + {plugins.map((plugin, index) => { + return ; + })} +
+
+ ); +}; + +export default PluginList; diff --git a/public/app/features/plugins/PluginListItem.test.tsx b/public/app/features/plugins/PluginListItem.test.tsx new file mode 100644 index 00000000000..175911c5e05 --- /dev/null +++ b/public/app/features/plugins/PluginListItem.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PluginListItem from './PluginListItem'; +import { getMockPlugin } from './__mocks__/pluginMocks'; + +const setup = (propOverrides?: object) => { + const props = Object.assign( + { + plugin: getMockPlugin(), + }, + propOverrides + ); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render has plugin section', () => { + const mockPlugin = getMockPlugin(); + mockPlugin.hasUpdate = true; + const wrapper = setup({ + plugin: mockPlugin, + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginListItem.tsx b/public/app/features/plugins/PluginListItem.tsx new file mode 100644 index 00000000000..05eac614fd5 --- /dev/null +++ b/public/app/features/plugins/PluginListItem.tsx @@ -0,0 +1,39 @@ +import React, { SFC } from 'react'; +import { Plugin } from 'app/types'; + +interface Props { + plugin: Plugin; +} + +const PluginListItem: SFC = props => { + const { plugin } = props; + + return ( +
  • + +
    +
    + + {plugin.type} +
    + {plugin.hasUpdate && ( +
    + Update available! +
    + )} +
    +
    +
    + +
    +
    +
    {plugin.name}
    +
    {`By ${plugin.info.author.name}`}
    +
    +
    +
    +
  • + ); +}; + +export default PluginListItem; diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx new file mode 100644 index 00000000000..452a89837c7 --- /dev/null +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { PluginListPage, Props } from './PluginListPage'; +import { NavModel, Plugin } from '../../types'; +import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + plugins: [] as Plugin[], + layoutMode: LayoutModes.Grid, + loadPlugins: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as PluginListPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx new file mode 100644 index 00000000000..de2968b126c --- /dev/null +++ b/public/app/features/plugins/PluginListPage.tsx @@ -0,0 +1,56 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from '../../core/components/PageHeader/PageHeader'; +import PluginActionBar from './PluginActionBar'; +import PluginList from './PluginList'; +import { NavModel, Plugin } from '../../types'; +import { loadPlugins } from './state/actions'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getLayoutMode, getPlugins } from './state/selectors'; +import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; + +export interface Props { + navModel: NavModel; + plugins: Plugin[]; + layoutMode: LayoutMode; + loadPlugins: typeof loadPlugins; +} + +export class PluginListPage extends PureComponent { + componentDidMount() { + this.fetchPlugins(); + } + + async fetchPlugins() { + await this.props.loadPlugins(); + } + + render() { + const { navModel, plugins, layoutMode } = this.props; + + return ( +
    + +
    + + {plugins && } +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + navModel: getNavModel(state.navIndex, 'plugins'), + plugins: getPlugins(state.plugins), + layoutMode: getLayoutMode(state.plugins), + }; +} + +const mapDispatchToProps = { + loadPlugins, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage)); diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts new file mode 100644 index 00000000000..d8dd67d5b61 --- /dev/null +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -0,0 +1,59 @@ +import { Plugin } from 'app/types'; + +export const getMockPlugins = (amount: number): Plugin[] => { + const plugins = []; + + for (let i = 0; i <= amount; i++) { + plugins.push({ + defaultNavUrl: 'some/url', + enabled: false, + hasUpdate: false, + id: `${i}`, + info: { + author: { + name: 'Grafana Labs', + url: 'url/to/GrafanaLabs', + }, + description: 'pretty decent plugin', + links: ['one link'], + logos: { small: 'small/logo', large: 'large/logo' }, + screenshots: `screenshot/${i}`, + updated: '2018-09-26', + version: '1', + }, + latestVersion: `1.${i}`, + name: `pretty cool plugin-${i}`, + pinned: false, + state: '', + type: '', + }); + } + + return plugins; +}; + +export const getMockPlugin = () => { + return { + defaultNavUrl: 'some/url', + enabled: false, + hasUpdate: false, + id: '1', + info: { + author: { + name: 'Grafana Labs', + url: 'url/to/GrafanaLabs', + }, + description: 'pretty decent plugin', + links: ['one link'], + logos: { small: 'small/logo', large: 'large/logo' }, + screenshots: 'screenshot/1', + updated: '2018-09-26', + version: '1', + }, + latestVersion: '1', + name: 'pretty cool plugin 1', + pinned: false, + state: '', + type: '', + }; +}; diff --git a/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap new file mode 100644 index 00000000000..30cb53cea27 --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginActionBar.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
    + + +
    + +`; diff --git a/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap new file mode 100644 index 00000000000..176304b7b11 --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginList.test.tsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    +
      + + + + + + +
    +
    +`; diff --git a/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap new file mode 100644 index 00000000000..fc0cc68c522 --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginListItem.test.tsx.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
  • + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + pretty cool plugin 1 +
    +
    + By Grafana Labs +
    +
    +
    +
    +
  • +`; + +exports[`Render should render has plugin section 1`] = ` +
  • + +
    +
    + +
    +
    + + Update available! + +
    +
    +
    +
    + +
    +
    +
    + pretty cool plugin 1 +
    +
    + By Grafana Labs +
    +
    +
    +
    +
  • +`; diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap new file mode 100644 index 00000000000..74b23d8850a --- /dev/null +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    + + +
    +
    +`; diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index fd19ea963b6..5be7593f68d 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,6 +1,5 @@ import './plugin_edit_ctrl'; import './plugin_page_ctrl'; -import './plugin_list_ctrl'; import './import_list/import_list'; import './ds_edit_ctrl'; import './ds_dashboards_ctrl'; diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html deleted file mode 100644 index 04b5bf9c791..00000000000 --- a/public/app/features/plugins/partials/plugin_list.html +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/public/app/features/plugins/plugin_list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts deleted file mode 100644 index 315252364cc..00000000000 --- a/public/app/features/plugins/plugin_list_ctrl.ts +++ /dev/null @@ -1,30 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; - -export class PluginListCtrl { - plugins: any[]; - tabIndex: number; - navModel: any; - searchQuery: string; - allPlugins: any[]; - - /** @ngInject */ - constructor(private backendSrv: any, $location, navModelSrv) { - this.tabIndex = 0; - this.navModel = navModelSrv.getNav('cfg', 'plugins', 0); - - this.backendSrv.get('api/plugins', { embedded: 0 }).then(plugins => { - this.plugins = plugins; - this.allPlugins = plugins; - }); - } - - onQueryUpdated() { - const regex = new RegExp(this.searchQuery, 'ig'); - this.plugins = _.filter(this.allPlugins, item => { - return regex.test(item.name) || regex.test(item.type); - }); - } -} - -angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl); diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts new file mode 100644 index 00000000000..24774c6061c --- /dev/null +++ b/public/app/features/plugins/state/actions.ts @@ -0,0 +1,51 @@ +import { Plugin, StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from '../../../core/services/backend_srv'; +import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; + +export enum ActionTypes { + LoadPlugins = 'LOAD_PLUGINS', + SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY', + SetLayoutMode = 'SET_LAYOUT_MODE', +} + +export interface LoadPluginsAction { + type: ActionTypes.LoadPlugins; + payload: Plugin[]; +} + +export interface SetPluginsSearchQueryAction { + type: ActionTypes.SetPluginsSearchQuery; + payload: string; +} + +export interface SetLayoutModeAction { + type: ActionTypes.SetLayoutMode; + payload: LayoutMode; +} + +export const setLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({ + type: ActionTypes.SetLayoutMode, + payload: mode, +}); + +export const setPluginsSearchQuery = (query: string): SetPluginsSearchQueryAction => ({ + type: ActionTypes.SetPluginsSearchQuery, + payload: query, +}); + +const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({ + type: ActionTypes.LoadPlugins, + payload: plugins, +}); + +export type Action = LoadPluginsAction | SetPluginsSearchQueryAction | SetLayoutModeAction; + +type ThunkResult = ThunkAction; + +export function loadPlugins(): ThunkResult { + return async dispatch => { + const result = await getBackendSrv().get('api/plugins', { embedded: 0 }); + dispatch(pluginsLoaded(result)); + }; +} diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts new file mode 100644 index 00000000000..1ca2880282c --- /dev/null +++ b/public/app/features/plugins/state/reducers.ts @@ -0,0 +1,27 @@ +import { Action, ActionTypes } from './actions'; +import { Plugin, PluginsState } from 'app/types'; +import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector'; + +export const initialState: PluginsState = { + plugins: [] as Plugin[], + searchQuery: '', + layoutMode: LayoutModes.Grid, +}; + +export const pluginsReducer = (state = initialState, action: Action): PluginsState => { + switch (action.type) { + case ActionTypes.LoadPlugins: + return { ...state, plugins: action.payload }; + + case ActionTypes.SetPluginsSearchQuery: + return { ...state, searchQuery: action.payload }; + + case ActionTypes.SetLayoutMode: + return { ...state, layoutMode: action.payload }; + } + return state; +}; + +export default { + plugins: pluginsReducer, +}; diff --git a/public/app/features/plugins/state/selectors.test.ts b/public/app/features/plugins/state/selectors.test.ts new file mode 100644 index 00000000000..09b1ce4c259 --- /dev/null +++ b/public/app/features/plugins/state/selectors.test.ts @@ -0,0 +1,31 @@ +import { getPlugins, getPluginsSearchQuery } from './selectors'; +import { initialState } from './reducers'; +import { getMockPlugins } from '../__mocks__/pluginMocks'; + +describe('Selectors', () => { + const mockState = initialState; + + it('should return search query', () => { + mockState.searchQuery = 'test'; + const query = getPluginsSearchQuery(mockState); + + expect(query).toEqual(mockState.searchQuery); + }); + + it('should return plugins', () => { + mockState.plugins = getMockPlugins(5); + mockState.searchQuery = ''; + + const plugins = getPlugins(mockState); + + expect(plugins).toEqual(mockState.plugins); + }); + + it('should filter plugins', () => { + mockState.searchQuery = 'plugin-1'; + + const plugins = getPlugins(mockState); + + expect(plugins.length).toEqual(1); + }); +}); diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts new file mode 100644 index 00000000000..e1d16462527 --- /dev/null +++ b/public/app/features/plugins/state/selectors.ts @@ -0,0 +1,10 @@ +export const getPlugins = state => { + const regex = new RegExp(state.searchQuery, 'i'); + + return state.plugins.filter(item => { + return regex.test(item.name) || regex.test(item.info.author.name) || regex.test(item.info.description); + }); +}; + +export const getPluginsSearchQuery = state => state.searchQuery; +export const getLayoutMode = state => state.layoutMode; diff --git a/public/app/features/templating/TextBoxVariable.ts b/public/app/features/templating/TextBoxVariable.ts new file mode 100644 index 00000000000..331ff4f95b8 --- /dev/null +++ b/public/app/features/templating/TextBoxVariable.ts @@ -0,0 +1,58 @@ +import { Variable, assignModelProperties, variableTypes } from './variable'; + +export class TextBoxVariable implements Variable { + query: string; + current: any; + options: any[]; + skipUrlSync: boolean; + + defaults = { + type: 'textbox', + name: '', + hide: 2, + label: '', + query: '', + current: {}, + options: [], + skipUrlSync: false, + }; + + /** @ngInject */ + constructor(private model, private variableSrv) { + assignModelProperties(this, model, this.defaults); + } + + getSaveModel() { + assignModelProperties(this.model, this, this.defaults); + return this.model; + } + + setValue(option) { + this.variableSrv.setOptionAsCurrent(this, option); + } + + updateOptions() { + this.options = [{ text: this.query.trim(), value: this.query.trim() }]; + this.current = this.options[0]; + return Promise.resolve(); + } + + dependsOn(variable) { + return false; + } + + setValueFromUrl(urlValue) { + this.query = urlValue; + return this.variableSrv.setOptionFromUrl(this, urlValue); + } + + getValueForUrl() { + return this.current.value; + } +} + +variableTypes['textbox'] = { + name: 'Text box', + ctor: TextBoxVariable, + description: 'Define a textbox variable, where users can enter any arbitrary string', +}; diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index 16465740642..b872fa6cd4a 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -9,6 +9,7 @@ import { DatasourceVariable } from './datasource_variable'; import { CustomVariable } from './custom_variable'; import { ConstantVariable } from './constant_variable'; import { AdhocVariable } from './adhoc_variable'; +import { TextBoxVariable } from './TextBoxVariable'; coreModule.factory('templateSrv', () => { return templateSrv; @@ -22,4 +23,5 @@ export { CustomVariable, ConstantVariable, AdhocVariable, + TextBoxVariable, }; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 0d8b0ace327..ac4450c20a2 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -155,6 +155,14 @@
    +
    +
    Text options
    +
    + Default value + +
    +
    +
    Query Options
    diff --git a/public/app/plugins/datasource/cloudwatch/partials/config.html b/public/app/plugins/datasource/cloudwatch/partials/config.html index a4df0c14807..e5ab0910cba 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/config.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -39,7 +39,7 @@
    - + Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 470153f5dd1..dbaa1c02952 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -6,6 +6,7 @@ import AlertRuleList from 'app/features/alerting/AlertRuleList'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; import ApiKeys from 'app/features/api-keys/ApiKeysPage'; +import PluginListPage from 'app/features/plugins/PluginListPage'; import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; import FolderPermissions from 'app/features/folders/FolderPermissions'; @@ -249,9 +250,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/plugins', { - templateUrl: 'public/app/features/plugins/partials/plugin_list.html', - controller: 'PluginListCtrl', - controllerAs: 'ctrl', + template: '', + resolve: { + component: () => PluginListPage, + }, }) .when('/plugins/:pluginId/edit', { templateUrl: 'public/app/features/plugins/partials/plugin_edit.html', diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 3988dad0cd8..6d1f5f74b4e 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers'; import apiKeysReducers from 'app/features/api-keys/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; import dashboardReducers from 'app/features/dashboard/state/reducers'; +import pluginReducers from 'app/features/plugins/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, @@ -15,6 +16,7 @@ const rootReducer = combineReducers({ ...apiKeysReducers, ...foldersReducers, ...dashboardReducers, + ...pluginReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 42460ecb9c6..b4e4e504a2b 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -6,9 +6,9 @@ import { FolderDTO, FolderState, FolderInfo } from './folders'; import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { DataSource } from './datasources'; -import { PluginMeta } from './plugins'; import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { User } from './user'; +import { PluginMeta, Plugin, PluginsState } from './plugins'; export { Team, @@ -39,6 +39,8 @@ export { ApiKeysState, NewApiKey, User, + Plugin, + PluginsState, }; export interface StoreState { diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index d26085f8e73..92bebfef8d4 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -12,8 +12,36 @@ export interface PluginInclude { } export interface PluginMetaInfo { + author: { + name: string; + url: string; + }; + description: string; + links: string[]; logos: { large: string; small: string; }; + screenshots: string; + updated: string; + version: string; +} + +export interface Plugin { + defaultNavUrl: string; + enabled: boolean; + hasUpdate: boolean; + id: string; + info: PluginMetaInfo; + latestVersion: string; + name: string; + pinned: boolean; + state: string; + type: string; +} + +export interface PluginsState { + plugins: Plugin[]; + searchQuery: string; + layoutMode: string; } diff --git a/public/sass/components/_form_select_box.scss b/public/sass/components/_form_select_box.scss index 0401f06eba8..4db294317f7 100644 --- a/public/sass/components/_form_select_box.scss +++ b/public/sass/components/_form_select_box.scss @@ -3,7 +3,7 @@ $select-menu-max-height: 300px; $select-item-font-size: $font-size-base; $select-item-bg: $dropdownBackground; $select-item-fg: $input-color; -$select-option-bg: $dropdownBackground; +$select-option-bg: $menu-dropdown-bg; $select-option-color: $input-color; $select-noresults-color: $text-color; $select-input-bg: $input-bg; @@ -82,20 +82,14 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive; width: auto; } + .Select-option { + border-left: 2px solid transparent; + } + .Select-option.is-focused { background-color: $dropdownLinkBackgroundHover; color: $dropdownLinkColorHover; - - &::before { - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 2px; - display: block; - content: ''; - background-image: linear-gradient(to bottom, #ffd500 0%, #ff4400 99%, #ff4400 100%); - } + @include left-brand-border-gradient(); } } diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 903193d8b10..d427ee203b8 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -69,7 +69,7 @@ } .datasource-picker { - min-width: 10rem; + min-width: 200px; } .timepicker { diff --git a/public/views/index.template.html b/public/views/index.template.html index ec51a12d34f..c39d5e08321 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -275,7 +275,10 @@ document.head.insertBefore(myCSS, document.head.childNodes[document.head.childNodes.length - 1].nextSibling); // switch loader to show all has loaded window.onload = function() { - document.getElementsByClassName("preloader")[0].className = "preloader preloader--done"; + var preloader = document.getElementsByClassName("preloader"); + if (preloader.length) { + preloader[0].className = "preloader preloader--done"; + } }; diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js index 07519cdd6c8..bfa4080da4c 100644 --- a/scripts/grunt/default_task.js +++ b/scripts/grunt/default_task.js @@ -1,5 +1,5 @@ // Lint and build CSS -module.exports = function(grunt) { +module.exports = function (grunt) { 'use strict'; grunt.registerTask('default', [ @@ -18,15 +18,16 @@ module.exports = function(grunt) { grunt.registerTask('precommit', [ 'sasslint', 'exec:tslint', + 'exec:tsc', 'no-only-tests' ]); - grunt.registerTask('no-only-tests', function() { + grunt.registerTask('no-only-tests', function () { var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js'); - files.forEach(function(spec) { + files.forEach(function (spec) { var rows = grunt.file.read(spec).split('\n'); - rows.forEach(function(row) { + rows.forEach(function (row) { if (row.indexOf('.only(') > 0) { grunt.log.errorlns(row); grunt.fail.warn('found only statement in test: ' + spec) diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index 92e530cd5fd..d01a993be67 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -1,8 +1,9 @@ -module.exports = function(config, grunt) { +module.exports = function (config, grunt) { 'use strict'; return { tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json', + tsc: 'yarn tsc --noEmit', jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2', webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js', };