mirror of https://github.com/grafana/grafana
Frontend: Remove Angular (#99760)
* chore(angularsupport): delete feature toggle to disable angular * feat(angular-support): remove config.angularSupportEnabled * chore(jest): remove angular from setup file * chore(angular): delete angular deprecation ui components * refactor(angular): move migration featureflags into migration notice * chore(dashboard): remove angular deprecation notices * chore(annotations): remove angular editor loader * feat(appwrapper): no more angular app loading * feat(pluginscatalog): clean up angular plugin warnings and logic * chore(angular): delete angular app and associated files * feat(plugins): delete old angular graph plugin * feat(plugins): delete old angular table panel * feat(frontend): remove unused appEvent type * feat(dashboards): clean up angular from panel options and menu * feat(plugins): remove graph and table-old from built in plugins and delete sdk * feat(frontend): remove angular related imports in routes and explore graph * feat(theme): remove angular panel styles from global styles * chore(i18n): run make i18n-extract * test(api_plugins_test): refresh snapshot due to deleting old graph and table plugins * chore(angulardeprecation): delete angular migration notice components and usage * test(frontend): clean up tests that assert rendering angular deprecation notices * chore(backend): remove autoMigrateOldPanels feature flag * chore(config): remove angularSupportEnabled from config preventing loading angular plugins * chore(graphpanel): remove autoMigrateGraphPanel from feature toggles * chore(tablepanel): delete autoMigrateTablePanel feature flag * chore(piechart): delete autoMigratePiechartPanel feature flag * chore(worldmappanel): remove autoMigrateWorldmapPanel feature toggle * chore(statpanel): remove autoMigrateStatPanel feature flag * feat(dashboards): remove automigrate feature flags and always auto migrate angular panels * test(pluginsintegration): fix failing loader test * test(frontend): wip: fix failures and skip erroring migration tests * chore(codeowners): remove deleted angular related files and directories * test(graphite): remove angular mock from test file * test(dashboards): skip failing exporter test, remove angularSupportEnabled flags * test(dashbaord): skip another failing panel menu test * Tests: fixes pkg/services/pluginsintegration/loader/loader_test.go (#100505) * Tests: fixes pkg/services/pluginsintegration/plugins_integration_test.go * Trigger Build * chore(dashboards): remove angularComponent from getPanelMenu, update test * feat(dashboards): remove all usage of AngularComponent and getAngularLoader * chore(betterer): refresh results file * feat(plugins): remove PluginAngularBadge component and usage * feat(datasource_srv): remove usage of getLegacyAngularInjector * feat(queryeditor): delete AngularQueryComponentScope type * Chore: removes Angular from plugin_loader * Chore: remove angular from getPlugin * Chore: fix i18n * Trigger Build * Chore: remove more Angular from importPanelPlugin * Chore: remove search options warning * Chore: remove and deprecate Angular related * chore(angular): remove angular dependencies from core and runtime * chore(runtime): delete angular injector * chore(data): delete angular scope from event bus * chore(plugin-catalog): remove code pushing app plugins angular config page * chore(yarn): refresh lock file * chore(frontend): remove ng-loader from webpack configs, remove systemjs cjs plugin * chore(navigation): remove tether-drop cleanup from GrafanaRouter, delete dependency * chore(runtime): delete AngularLoader * chore(betterer): refresh results file * chore(betterer): fix out of sync results file * feat(query): fix type and import errors in QueryEditorRow * test(dashboards): delete skipped angular related tests * Tests: add back tests and fix betterer * Tests: fix broken test * Trigger build * chore(i18n): remove angular deprecation related strings * test: clean up connections and plugins catalog tests * chore(betterer): update results file --------- Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>pull/103443/head
parent
b1198b92c6
commit
f96e4e9ad2
@ -0,0 +1,63 @@ |
||||
import { PanelPlugin, PluginMeta, PluginType } from '@grafana/data'; |
||||
|
||||
import { throwIfAngular } from './throwIfAngular'; |
||||
|
||||
const plugin: PluginMeta = { |
||||
id: 'test', |
||||
name: 'Test', |
||||
type: PluginType.datasource, |
||||
info: { |
||||
author: { name: 'Test', url: 'https://test.com' }, |
||||
description: 'Test', |
||||
links: [], |
||||
logos: { large: '', small: '' }, |
||||
screenshots: [], |
||||
updated: '2021-01-01', |
||||
version: '1.0.0', |
||||
}, |
||||
module: 'test', |
||||
baseUrl: 'test', |
||||
}; |
||||
|
||||
describe('throwIfAngular', () => { |
||||
it('should throw if angular plugin', () => { |
||||
const underTest = { ...plugin, angular: { detected: true, hideDeprecation: false } }; |
||||
expect(() => throwIfAngular(underTest)).toThrow('Angular plugins are not supported'); |
||||
}); |
||||
|
||||
it('should throw if angular plugin', () => { |
||||
const underTest = { ...plugin, angularDetected: true }; |
||||
expect(() => throwIfAngular(underTest)).toThrow('Angular plugins are not supported'); |
||||
}); |
||||
|
||||
it('should throw if angular panel', () => { |
||||
const underTest = new PanelPlugin(null); |
||||
underTest.angularPanelCtrl = {}; |
||||
expect(() => throwIfAngular(underTest)).toThrow('Angular plugins are not supported'); |
||||
}); |
||||
|
||||
it('should throw if angular module', () => { |
||||
const underTest: System.Module = { PanelCtrl: {} }; |
||||
expect(() => throwIfAngular(underTest)).toThrow('Angular plugins are not supported'); |
||||
}); |
||||
|
||||
it('should throw if angular module', () => { |
||||
const underTest: System.Module = { ConfigCtrl: {} }; |
||||
expect(() => throwIfAngular(underTest)).toThrow('Angular plugins are not supported'); |
||||
}); |
||||
|
||||
it('should not throw if not angular plugin', () => { |
||||
const underTest = { ...plugin, angular: { detected: false, hideDeprecation: false } }; |
||||
expect(() => throwIfAngular(underTest)).not.toThrow(); |
||||
}); |
||||
|
||||
it('should not throw if not angular panel', () => { |
||||
const underTest = new PanelPlugin(null); |
||||
expect(() => throwIfAngular(underTest)).not.toThrow(); |
||||
}); |
||||
|
||||
it('should not throw if not angular module', () => { |
||||
const underTest: System.Module = {}; |
||||
expect(() => throwIfAngular(underTest)).not.toThrow(); |
||||
}); |
||||
}); |
@ -0,0 +1,15 @@ |
||||
import { PanelPlugin } from '../panel/PanelPlugin'; |
||||
import { PluginMeta } from '../types/plugin'; |
||||
|
||||
export function throwIfAngular(module?: System.Module): void; |
||||
export function throwIfAngular(panel?: PanelPlugin): void; |
||||
export function throwIfAngular(plugin?: PluginMeta): void; |
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function throwIfAngular(data?: any): void { |
||||
const isAngularPlugin = data?.angular?.detected ?? data?.angularDetected ?? false; |
||||
const isAngularPanel = data?.angularPanelCtrl ?? false; |
||||
const isAngularModule = data.PanelCtrl ?? data?.ConfigCtrl ?? false; |
||||
if (isAngularPlugin || isAngularPanel || isAngularModule) { |
||||
throw new Error('Angular plugins are not supported'); |
||||
} |
||||
} |
@ -1,87 +0,0 @@ |
||||
/** |
||||
* Used to enable rendering of Angular components within a |
||||
* React component without losing proper typings. |
||||
* |
||||
* @example |
||||
* ```typescript
|
||||
* class Component extends PureComponent<Props> { |
||||
* element: HTMLElement; |
||||
* angularComponent: AngularComponent; |
||||
* |
||||
* componentDidMount() { |
||||
* const template = '<angular-component />' // angular template here;
|
||||
* const scopeProps = { ctrl: angularController }; // angular scope properties here
|
||||
* const loader = getAngularLoader(); |
||||
* this.angularComponent = loader.load(this.element, scopeProps, template); |
||||
* } |
||||
* |
||||
* componentWillUnmount() { |
||||
* if (this.angularComponent) { |
||||
* this.angularComponent.destroy(); |
||||
* } |
||||
* } |
||||
* |
||||
* render() { |
||||
* return ( |
||||
* <div ref={element => (this.element = element)} /> |
||||
* ); |
||||
* } |
||||
* } |
||||
* ``` |
||||
* |
||||
* @public |
||||
*/ |
||||
export interface AngularComponent { |
||||
/** |
||||
* Should be called when the React component will unmount. |
||||
*/ |
||||
destroy(): void; |
||||
/** |
||||
* Can be used to trigger a re-render of the Angular component. |
||||
*/ |
||||
digest(): void; |
||||
/** |
||||
* Used to access the Angular scope from the React component. |
||||
*/ |
||||
getScope(): any; |
||||
} |
||||
|
||||
/** |
||||
* Used to load an Angular component from the context of a React component. |
||||
* Please see the {@link AngularComponent} for a proper example. |
||||
* |
||||
* @public |
||||
*/ |
||||
export interface AngularLoader { |
||||
/** |
||||
* |
||||
* @param elem - the element that the Angular component will be loaded into. |
||||
* @param scopeProps - values that will be accessed via the Angular scope. |
||||
* @param template - template used by the Angular component. |
||||
*/ |
||||
load(elem: any, scopeProps: any, template: string): AngularComponent; |
||||
} |
||||
|
||||
let instance: AngularLoader; |
||||
|
||||
/** |
||||
* Used during startup by Grafana to set the AngularLoader so it is available |
||||
* via the {@link getAngularLoader} to the rest of the application. |
||||
* |
||||
* @internal |
||||
*/ |
||||
export function setAngularLoader(v: AngularLoader) { |
||||
instance = v; |
||||
} |
||||
|
||||
/** |
||||
* Used to retrieve the {@link AngularLoader} that enables the use of Angular |
||||
* components within a React component. |
||||
* |
||||
* Please see the {@link AngularComponent} for a proper example. |
||||
* |
||||
* @public |
||||
*/ |
||||
export function getAngularLoader(): AngularLoader { |
||||
return instance; |
||||
} |
@ -1,23 +0,0 @@ |
||||
import { auto } from 'angular'; |
||||
|
||||
let singleton: auto.IInjectorService; |
||||
|
||||
/** |
||||
* Used during startup by Grafana to temporarily expose the angular injector to |
||||
* pure javascript plugins using {@link getLegacyAngularInjector}. |
||||
* |
||||
* @internal |
||||
*/ |
||||
export const setLegacyAngularInjector = (instance: auto.IInjectorService) => { |
||||
singleton = instance; |
||||
}; |
||||
|
||||
/** |
||||
* WARNING: this function provides a temporary way for plugins to access anything in the |
||||
* angular injector. While the migration from angular to react continues, there are a few |
||||
* options that do not yet have good alternatives. Note that use of this function will |
||||
* be removed in the future. |
||||
* |
||||
* @beta |
||||
*/ |
||||
export const getLegacyAngularInjector = (): auto.IInjectorService => singleton; |
@ -1,39 +0,0 @@ |
||||
import { css } from '@emotion/react'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
|
||||
export function getAgularPanelStyles(theme: GrafanaTheme2) { |
||||
return css({ |
||||
'.panel-options-group': { |
||||
borderBottom: `1px solid ${theme.colors.border.weak}`, |
||||
}, |
||||
|
||||
'.panel-options-group__header': { |
||||
padding: theme.spacing(1, 2, 1, 1), |
||||
position: 'relative', |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
cursor: 'pointer', |
||||
fontWeight: 500, |
||||
color: theme.colors.text.primary, |
||||
|
||||
'&:hover': { |
||||
background: theme.colors.emphasize(theme.colors.background.primary, 0.03), |
||||
}, |
||||
}, |
||||
|
||||
'.panel-options-group__icon': { |
||||
color: theme.colors.text.secondary, |
||||
marginRight: theme.spacing(1), |
||||
padding: theme.spacing(0, 0.9, 0, 0.6), |
||||
}, |
||||
|
||||
'.panel-options-group__title': { |
||||
position: 'relative', |
||||
}, |
||||
|
||||
'.panel-options-group__body': { |
||||
padding: theme.spacing(1, 2, 1, 4), |
||||
}, |
||||
}); |
||||
} |
|
File diff suppressed because it is too large
Load Diff
@ -1,166 +0,0 @@ |
||||
import 'angular'; |
||||
import 'angular-route'; |
||||
import 'angular-sanitize'; |
||||
import 'angular-bindonce'; |
||||
import 'vendor/bootstrap/bootstrap'; |
||||
|
||||
import angular from 'angular'; // eslint-disable-line no-duplicate-imports
|
||||
import { extend } from 'lodash'; |
||||
|
||||
import { getTemplateSrv } from '@grafana/runtime'; |
||||
import { coreModule, angularModules } from 'app/angular/core_module'; |
||||
import appEvents from 'app/core/app_events'; |
||||
import { config } from 'app/core/config'; |
||||
import { contextSrv } from 'app/core/services/context_srv'; |
||||
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; |
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; |
||||
import { setAngularPanelReactWrapper } from 'app/features/plugins/importPanelPlugin'; |
||||
import { SystemJS } from 'app/features/plugins/loader/systemjs'; |
||||
import { buildImportMap } from 'app/features/plugins/loader/utils'; |
||||
import * as sdk from 'app/plugins/sdk'; |
||||
|
||||
import { registerAngularDirectives } from './angular_wrappers'; |
||||
import { initAngularRoutingBridge } from './bridgeReactAngularRouting'; |
||||
import { monkeyPatchInjectorWithPreAssignedBindings } from './injectorMonkeyPatch'; |
||||
import { getAngularPanelReactWrapper } from './panel/AngularPanelReactWrapper'; |
||||
import { promiseToDigest } from './promiseToDigest'; |
||||
import { registerComponents } from './registerComponents'; |
||||
|
||||
// Angular plugin dependencies map
|
||||
const importMap = { |
||||
angular: { |
||||
...angular, |
||||
default: angular, |
||||
}, |
||||
'app/core/core_module': { |
||||
default: coreModule, |
||||
__useDefault: true, |
||||
}, |
||||
'app/core/core': { |
||||
appEvents: appEvents, |
||||
contextSrv: contextSrv, |
||||
coreModule: coreModule, |
||||
}, |
||||
'app/plugins/sdk': sdk, |
||||
'app/core/utils/promiseToDigest': { promiseToDigest }, |
||||
} as Record<string, System.Module>; |
||||
|
||||
export class AngularApp { |
||||
ngModuleDependencies: any[]; |
||||
preBootModules: any[]; |
||||
registerFunctions: any; |
||||
|
||||
constructor() { |
||||
this.preBootModules = []; |
||||
this.ngModuleDependencies = []; |
||||
this.registerFunctions = {}; |
||||
} |
||||
|
||||
init() { |
||||
const app = angular.module('grafana', []); |
||||
|
||||
setAngularPanelReactWrapper(getAngularPanelReactWrapper); |
||||
|
||||
app.config([ |
||||
'$controllerProvider', |
||||
'$compileProvider', |
||||
'$filterProvider', |
||||
'$httpProvider', |
||||
'$provide', |
||||
'$sceDelegateProvider', |
||||
( |
||||
$controllerProvider: angular.IControllerProvider, |
||||
$compileProvider: angular.ICompileProvider, |
||||
$filterProvider: angular.IFilterProvider, |
||||
$httpProvider: angular.IHttpProvider, |
||||
$provide: angular.auto.IProvideService, |
||||
$sceDelegateProvider: angular.ISCEDelegateProvider |
||||
) => { |
||||
if (config.buildInfo.env !== 'development') { |
||||
$compileProvider.debugInfoEnabled(false); |
||||
} |
||||
|
||||
$httpProvider.useApplyAsync(true); |
||||
|
||||
if (Boolean(config.pluginsCDNBaseURL)) { |
||||
$sceDelegateProvider.trustedResourceUrlList(['self', `${config.pluginsCDNBaseURL}/**`]); |
||||
} |
||||
|
||||
this.registerFunctions.controller = $controllerProvider.register; |
||||
this.registerFunctions.directive = $compileProvider.directive; |
||||
this.registerFunctions.factory = $provide.factory; |
||||
this.registerFunctions.service = $provide.service; |
||||
this.registerFunctions.filter = $filterProvider.register; |
||||
|
||||
$provide.decorator('$http', [ |
||||
'$delegate', |
||||
'$templateCache', |
||||
($delegate: any, $templateCache: any) => { |
||||
const get = $delegate.get; |
||||
$delegate.get = (url: string, config: any) => { |
||||
if (url.match(/\.html$/)) { |
||||
// some template's already exist in the cache
|
||||
if (!$templateCache.get(url)) { |
||||
url += '?v=' + new Date().getTime(); |
||||
} |
||||
} |
||||
return get(url, config); |
||||
}; |
||||
return $delegate; |
||||
}, |
||||
]); |
||||
}, |
||||
]); |
||||
|
||||
this.ngModuleDependencies = ['grafana.core', 'ngSanitize', 'grafana', 'pasvaz.bindonce', 'react']; |
||||
|
||||
// makes it possible to add dynamic stuff
|
||||
angularModules.forEach((m: angular.IModule) => { |
||||
this.useModule(m); |
||||
}); |
||||
|
||||
// register react angular wrappers
|
||||
angular.module('grafana.services').service('dashboardLoaderSrv', DashboardLoaderSrv); |
||||
|
||||
coreModule.factory('timeSrv', () => getTimeSrv()); |
||||
coreModule.factory('templateSrv', () => getTemplateSrv()); |
||||
|
||||
registerAngularDirectives(); |
||||
registerComponents(); |
||||
initAngularRoutingBridge(); |
||||
|
||||
const imports = buildImportMap(importMap); |
||||
// pass the map of module names so systemjs can resolve them
|
||||
SystemJS.addImportMap({ imports }); |
||||
|
||||
// disable tool tip animation
|
||||
$.fn.tooltip.defaults.animation = false; |
||||
} |
||||
|
||||
useModule(module: angular.IModule) { |
||||
if (this.preBootModules) { |
||||
this.preBootModules.push(module); |
||||
} else { |
||||
extend(module, this.registerFunctions); |
||||
} |
||||
this.ngModuleDependencies.push(module.name); |
||||
return module; |
||||
} |
||||
|
||||
bootstrap() { |
||||
const injector = angular.bootstrap(document.getElementById('ngRoot')!, this.ngModuleDependencies); |
||||
|
||||
monkeyPatchInjectorWithPreAssignedBindings(injector); |
||||
|
||||
injector.invoke(() => { |
||||
this.preBootModules.forEach((module) => { |
||||
extend(module, this.registerFunctions); |
||||
}); |
||||
|
||||
// I don't know
|
||||
return () => {}; |
||||
}); |
||||
|
||||
return injector; |
||||
} |
||||
} |
@ -1,209 +0,0 @@ |
||||
import { HistoryWrapper, locationService, setLocationService } from '@grafana/runtime'; |
||||
|
||||
import { AngularLocationWrapper } from './AngularLocationWrapper'; |
||||
|
||||
// The methods in this file are deprecated
|
||||
// Stub the deprecation warning here to prevent polluting the test output
|
||||
jest.mock('@grafana/data', () => ({ |
||||
...jest.requireActual('@grafana/data'), |
||||
deprecationWarning: () => {}, |
||||
})); |
||||
|
||||
describe('AngularLocationWrapper', () => { |
||||
const { location } = window; |
||||
|
||||
beforeEach(() => { |
||||
setLocationService(new HistoryWrapper()); |
||||
}); |
||||
|
||||
beforeAll(() => { |
||||
// @ts-ignore
|
||||
delete window.location; |
||||
|
||||
window.location = { |
||||
...location, |
||||
hash: '#hash', |
||||
host: 'localhost:3000', |
||||
hostname: 'localhost', |
||||
href: 'http://www.domain.com:9877/path/b?search=a&b=c&d#hash', |
||||
origin: 'http://www.domain.com:9877', |
||||
pathname: '/path/b', |
||||
port: '9877', |
||||
protocol: 'http:', |
||||
search: '?search=a&b=c&d', |
||||
}; |
||||
}); |
||||
|
||||
afterAll(() => { |
||||
window.location = location; |
||||
}); |
||||
|
||||
const wrapper = new AngularLocationWrapper(); |
||||
it('should provide common getters', () => { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
|
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash'); |
||||
expect(wrapper.protocol()).toBe('http'); |
||||
expect(wrapper.host()).toBe('www.domain.com'); |
||||
expect(wrapper.port()).toBe(9877); |
||||
expect(wrapper.path()).toBe('/path/b'); |
||||
expect(wrapper.search()).toEqual({ search: 'a', b: 'c', d: true }); |
||||
expect(wrapper.hash()).toBe('hash'); |
||||
expect(wrapper.url()).toBe('/path/b?search=a&b=c&d#hash'); |
||||
}); |
||||
|
||||
describe('path', () => { |
||||
it('should change path', function () { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
wrapper.path('/new/path'); |
||||
|
||||
expect(wrapper.path()).toBe('/new/path'); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash'); |
||||
}); |
||||
|
||||
it('should not break on numeric values', function () { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
wrapper.path(1); |
||||
expect(wrapper.path()).toBe('/1'); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/1?search=a&b=c&d#hash'); |
||||
}); |
||||
|
||||
it('should allow using 0 as path', function () { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
wrapper.path(0); |
||||
expect(wrapper.path()).toBe('/0'); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/0?search=a&b=c&d#hash'); |
||||
}); |
||||
it('should set to empty path on null value', function () { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
wrapper.path('/foo'); |
||||
expect(wrapper.path()).toBe('/foo'); |
||||
wrapper.path(null); |
||||
expect(wrapper.path()).toBe('/'); |
||||
}); |
||||
}); |
||||
|
||||
describe('search', () => { |
||||
it('should accept string', function () { |
||||
locationService.push('/path/b'); |
||||
wrapper.search('x=y&c'); |
||||
expect(wrapper.search()).toEqual({ x: 'y', c: true }); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c'); |
||||
}); |
||||
|
||||
it('search() should accept object', function () { |
||||
locationService.push('/path/b'); |
||||
wrapper.search({ one: '1', two: true }); |
||||
expect(wrapper.search()).toEqual({ one: '1', two: true }); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two'); |
||||
}); |
||||
|
||||
it('should copy object', function () { |
||||
locationService.push('/path/b'); |
||||
const obj: Record<string, unknown> = { one: '1', two: true, three: null }; |
||||
wrapper.search(obj); |
||||
expect(obj).toEqual({ one: '1', two: true, three: null }); |
||||
obj.one = 'changed'; |
||||
|
||||
expect(wrapper.search()).toEqual({ one: '1', two: true }); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two'); |
||||
}); |
||||
|
||||
it('should change single parameter', function () { |
||||
wrapper.search({ id: 'old', preserved: true }); |
||||
wrapper.search('id', 'new'); |
||||
|
||||
expect(wrapper.search()).toEqual({ id: 'new', preserved: true }); |
||||
}); |
||||
|
||||
it('should remove single parameter', function () { |
||||
wrapper.search({ id: 'old', preserved: true }); |
||||
wrapper.search('id', null); |
||||
|
||||
expect(wrapper.search()).toEqual({ preserved: true }); |
||||
}); |
||||
|
||||
it('should remove multiple parameters', function () { |
||||
locationService.push('/path/b'); |
||||
wrapper.search({ one: '1', two: true }); |
||||
expect(wrapper.search()).toEqual({ one: '1', two: true }); |
||||
|
||||
wrapper.search({ one: null, two: null }); |
||||
expect(wrapper.search()).toEqual({}); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b'); |
||||
}); |
||||
|
||||
it('should accept numeric keys', function () { |
||||
locationService.push('/path/b'); |
||||
wrapper.search({ 1: 'one', 2: 'two' }); |
||||
expect(wrapper.search()).toEqual({ '1': 'one', '2': 'two' }); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b?1=one&2=two'); |
||||
}); |
||||
|
||||
it('should handle multiple value', function () { |
||||
wrapper.search('a&b'); |
||||
expect(wrapper.search()).toEqual({ a: true, b: true }); |
||||
|
||||
wrapper.search('a', null); |
||||
|
||||
expect(wrapper.search()).toEqual({ b: true }); |
||||
|
||||
wrapper.search('b', undefined); |
||||
expect(wrapper.search()).toEqual({}); |
||||
}); |
||||
|
||||
it('should handle single value', function () { |
||||
wrapper.search('ignore'); |
||||
expect(wrapper.search()).toEqual({ ignore: true }); |
||||
wrapper.search(1); |
||||
expect(wrapper.search()).toEqual({ 1: true }); |
||||
}); |
||||
}); |
||||
|
||||
describe('url', () => { |
||||
it('should change the path, search and hash', function () { |
||||
wrapper.url('/some/path?a=b&c=d#hhh'); |
||||
expect(wrapper.url()).toBe('/some/path?a=b&c=d#hhh'); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh'); |
||||
expect(wrapper.path()).toBe('/some/path'); |
||||
expect(wrapper.search()).toEqual({ a: 'b', c: 'd' }); |
||||
expect(wrapper.hash()).toBe('hhh'); |
||||
}); |
||||
|
||||
it('should change only hash when no search and path specified', function () { |
||||
locationService.push('/path/b?search=a&b=c&d'); |
||||
wrapper.url('#some-hash'); |
||||
|
||||
expect(wrapper.hash()).toBe('some-hash'); |
||||
expect(wrapper.url()).toBe('/path/b?search=a&b=c&d#some-hash'); |
||||
expect(wrapper.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#some-hash'); |
||||
}); |
||||
|
||||
it('should change only search and hash when no path specified', function () { |
||||
locationService.push('/path/b'); |
||||
wrapper.url('?a=b'); |
||||
|
||||
expect(wrapper.search()).toEqual({ a: 'b' }); |
||||
expect(wrapper.hash()).toBe(''); |
||||
expect(wrapper.path()).toBe('/path/b'); |
||||
}); |
||||
|
||||
it('should reset search and hash when only path specified', function () { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
wrapper.url('/new/path'); |
||||
|
||||
expect(wrapper.path()).toBe('/new/path'); |
||||
expect(wrapper.search()).toEqual({}); |
||||
expect(wrapper.hash()).toBe(''); |
||||
}); |
||||
|
||||
it('should change path when empty string specified', function () { |
||||
locationService.push('/path/b?search=a&b=c&d#hash'); |
||||
wrapper.url(''); |
||||
|
||||
expect(wrapper.path()).toBe('/'); |
||||
expect(wrapper.search()).toEqual({}); |
||||
expect(wrapper.hash()).toBe(''); |
||||
}); |
||||
}); |
||||
}); |
@ -1,149 +0,0 @@ |
||||
import { deprecationWarning, urlUtil } from '@grafana/data'; |
||||
import { locationSearchToObject, locationService, navigationLogger } from '@grafana/runtime'; |
||||
|
||||
// Ref: https://github.com/angular/angular.js/blob/ae8e903edf88a83fedd116ae02c0628bf72b150c/src/ng/location.js#L5
|
||||
const DEFAULT_PORTS: Record<string, number> = { http: 80, https: 443, ftp: 21 }; |
||||
|
||||
export class AngularLocationWrapper { |
||||
constructor() { |
||||
this.absUrl = this.wrapInDeprecationWarning(this.absUrl); |
||||
this.hash = this.wrapInDeprecationWarning(this.hash); |
||||
this.host = this.wrapInDeprecationWarning(this.host); |
||||
this.path = this.wrapInDeprecationWarning(this.path); |
||||
this.port = this.wrapInDeprecationWarning(this.port, 'window.location'); |
||||
this.protocol = this.wrapInDeprecationWarning(this.protocol, 'window.location'); |
||||
this.replace = this.wrapInDeprecationWarning(this.replace); |
||||
this.search = this.wrapInDeprecationWarning(this.search); |
||||
this.state = this.wrapInDeprecationWarning(this.state); |
||||
this.url = this.wrapInDeprecationWarning(this.url); |
||||
} |
||||
|
||||
wrapInDeprecationWarning(fn: Function, replacement?: string) { |
||||
let self = this; |
||||
|
||||
return function wrapper() { |
||||
deprecationWarning('$location', fn.name, replacement || 'locationService'); |
||||
return fn.apply(self, arguments); |
||||
}; |
||||
} |
||||
|
||||
absUrl(): string { |
||||
return `${window.location.origin}${this.url()}`; |
||||
} |
||||
|
||||
hash(newHash?: string | null) { |
||||
navigationLogger('AngularLocationWrapper', false, 'Angular compat layer: hash'); |
||||
|
||||
if (!newHash) { |
||||
return locationService.getLocation().hash.slice(1); |
||||
} else { |
||||
throw new Error('AngularLocationWrapper method not implemented.'); |
||||
} |
||||
} |
||||
|
||||
host(): string { |
||||
return new URL(window.location.href).hostname; |
||||
} |
||||
|
||||
path(pathname?: any) { |
||||
navigationLogger('AngularLocationWrapper', false, 'Angular compat layer: path'); |
||||
|
||||
const location = locationService.getLocation(); |
||||
|
||||
if (pathname !== undefined && pathname !== null) { |
||||
let parsedPath = String(pathname); |
||||
parsedPath = parsedPath.startsWith('/') ? parsedPath : `/${parsedPath}`; |
||||
const url = new URL(`${window.location.origin}${parsedPath}`); |
||||
|
||||
locationService.push({ |
||||
pathname: url.pathname, |
||||
search: url.search.length > 0 ? url.search : location.search, |
||||
hash: url.hash.length > 0 ? url.hash : location.hash, |
||||
}); |
||||
return this; |
||||
} |
||||
|
||||
if (pathname === null) { |
||||
locationService.push('/'); |
||||
return this; |
||||
} |
||||
|
||||
return location.pathname; |
||||
} |
||||
|
||||
port(): number | null { |
||||
const url = new URL(window.location.href); |
||||
return parseInt(url.port, 10) || DEFAULT_PORTS[url.protocol] || null; |
||||
} |
||||
|
||||
protocol(): string { |
||||
return new URL(window.location.href).protocol.slice(0, -1); |
||||
} |
||||
|
||||
replace() { |
||||
throw new Error('AngularLocationWrapper method not implemented.'); |
||||
} |
||||
|
||||
search(search?: any, paramValue?: any) { |
||||
navigationLogger('AngularLocationWrapper', false, 'Angular compat layer: search'); |
||||
if (!search) { |
||||
return locationService.getSearchObject(); |
||||
} |
||||
|
||||
if (search && arguments.length > 1) { |
||||
locationService.partial({ |
||||
[search]: paramValue, |
||||
}); |
||||
|
||||
return this; |
||||
} |
||||
|
||||
if (search) { |
||||
let newQuery; |
||||
|
||||
if (typeof search === 'object') { |
||||
newQuery = { ...search }; |
||||
} else { |
||||
newQuery = locationSearchToObject(search); |
||||
} |
||||
|
||||
for (const key in newQuery) { |
||||
// removing params with null | undefined
|
||||
if (newQuery[key] === null || newQuery[key] === undefined) { |
||||
delete newQuery[key]; |
||||
} |
||||
} |
||||
|
||||
const updatedUrl = urlUtil.renderUrl(locationService.getLocation().pathname, newQuery); |
||||
locationService.push(updatedUrl); |
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
state(state?: any) { |
||||
navigationLogger('AngularLocationWrapper', false, 'Angular compat layer: state'); |
||||
throw new Error('AngularLocationWrapper method not implemented.'); |
||||
} |
||||
|
||||
url(newUrl?: any) { |
||||
navigationLogger('AngularLocationWrapper', false, 'Angular compat layer: url'); |
||||
|
||||
if (newUrl !== undefined) { |
||||
if (newUrl.startsWith('#')) { |
||||
locationService.push({ ...locationService.getLocation(), hash: newUrl }); |
||||
} else if (newUrl.startsWith('?')) { |
||||
locationService.push({ ...locationService.getLocation(), search: newUrl }); |
||||
} else if (newUrl.trim().length === 0) { |
||||
locationService.push('/'); |
||||
} else { |
||||
locationService.push(newUrl); |
||||
} |
||||
|
||||
return locationService; |
||||
} |
||||
|
||||
const location = locationService.getLocation(); |
||||
return `${location.pathname}${location.search}${location.hash}`; |
||||
} |
||||
} |
@ -1,15 +0,0 @@ |
||||
import { forwardRef } from 'react'; |
||||
|
||||
export const AngularRoot = forwardRef<HTMLDivElement, {}>((props, ref) => { |
||||
return ( |
||||
<div |
||||
id="ngRoot" |
||||
ref={ref} |
||||
dangerouslySetInnerHTML={{ |
||||
__html: '<grafana-app ng-cloak></grafana-app>', |
||||
}} |
||||
/> |
||||
); |
||||
}); |
||||
|
||||
AngularRoot.displayName = 'AngularRoot'; |
@ -1,158 +0,0 @@ |
||||
import { IRootScopeService, IAngularEvent, auto } from 'angular'; |
||||
import $ from 'jquery'; |
||||
import _ from 'lodash'; // eslint-disable-line lodash/import-scope
|
||||
|
||||
import { AppEvent } from '@grafana/data'; |
||||
import { setLegacyAngularInjector, setAngularLoader } from '@grafana/runtime'; |
||||
import { colors } from '@grafana/ui'; |
||||
import coreModule from 'app/angular/core_module'; |
||||
import { AngularLoader } from 'app/angular/services/AngularLoader'; |
||||
import appEvents from 'app/core/app_events'; |
||||
import config from 'app/core/config'; |
||||
import { ContextSrv } from 'app/core/services/context_srv'; |
||||
import { AppEventEmitter, AppEventConsumer } from 'app/types'; |
||||
|
||||
import { UtilSrv } from './services/UtilSrv'; |
||||
|
||||
export type GrafanaRootScope = IRootScopeService & AppEventEmitter & AppEventConsumer & { colors: string[] }; |
||||
|
||||
export class GrafanaCtrl { |
||||
static $inject = ['$scope', 'utilSrv', '$rootScope', 'contextSrv', 'angularLoader', '$injector']; |
||||
|
||||
constructor( |
||||
$scope: any, |
||||
utilSrv: UtilSrv, |
||||
$rootScope: GrafanaRootScope, |
||||
contextSrv: ContextSrv, |
||||
angularLoader: AngularLoader, |
||||
$injector: auto.IInjectorService |
||||
) { |
||||
// make angular loader service available to react components
|
||||
setAngularLoader(angularLoader); |
||||
setLegacyAngularInjector($injector); |
||||
|
||||
$scope.init = () => { |
||||
$scope.contextSrv = contextSrv; |
||||
$scope.appSubUrl = config.appSubUrl; |
||||
$scope._ = _; |
||||
utilSrv.init(); |
||||
}; |
||||
|
||||
$rootScope.colors = colors; |
||||
|
||||
$rootScope.onAppEvent = function <T>( |
||||
event: AppEvent<T> | string, |
||||
callback: (event: IAngularEvent, ...args: any[]) => void, |
||||
localScope?: any |
||||
) { |
||||
let unbind; |
||||
if (typeof event === 'string') { |
||||
unbind = $rootScope.$on(event, callback); |
||||
} else { |
||||
unbind = $rootScope.$on(event.name, callback); |
||||
} |
||||
|
||||
let callerScope = this; |
||||
if (callerScope.$id === 1 && !localScope) { |
||||
console.warn('warning rootScope onAppEvent called without localscope'); |
||||
} |
||||
if (localScope) { |
||||
callerScope = localScope; |
||||
} |
||||
callerScope.$on('$destroy', unbind); |
||||
}; |
||||
|
||||
$rootScope.appEvent = <T>(event: AppEvent<T> | string, payload?: T | any) => { |
||||
if (typeof event === 'string') { |
||||
$rootScope.$emit(event, payload); |
||||
appEvents.emit(event, payload); |
||||
} else { |
||||
$rootScope.$emit(event.name, payload); |
||||
appEvents.emit(event, payload); |
||||
} |
||||
}; |
||||
|
||||
$scope.init(); |
||||
} |
||||
} |
||||
|
||||
export function grafanaAppDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
controller: GrafanaCtrl, |
||||
link: (scope: IRootScopeService & AppEventEmitter, elem: JQuery) => { |
||||
const body = $('body'); |
||||
// see https://github.com/zenorocha/clipboard.js/issues/155
|
||||
$.fn.modal.Constructor.prototype.enforceFocus = () => {}; |
||||
|
||||
// handle in active view state class
|
||||
let lastActivity = new Date().getTime(); |
||||
let activeUser = true; |
||||
const inActiveTimeLimit = 60 * 5000; |
||||
|
||||
function checkForInActiveUser() { |
||||
if (!activeUser) { |
||||
return; |
||||
} |
||||
// only go to activity low mode on dashboard page
|
||||
if (!body.hasClass('page-dashboard')) { |
||||
return; |
||||
} |
||||
|
||||
if (new Date().getTime() - lastActivity > inActiveTimeLimit) { |
||||
activeUser = false; |
||||
body.addClass('view-mode--inactive'); |
||||
} |
||||
} |
||||
|
||||
function userActivityDetected() { |
||||
lastActivity = new Date().getTime(); |
||||
if (!activeUser) { |
||||
activeUser = true; |
||||
body.removeClass('view-mode--inactive'); |
||||
} |
||||
} |
||||
|
||||
// mouse and keyboard is user activity
|
||||
body.mousemove(userActivityDetected); |
||||
body.keydown(userActivityDetected); |
||||
// set useCapture = true to catch event here
|
||||
document.addEventListener('wheel', userActivityDetected, { capture: true, passive: true }); |
||||
// treat tab change as activity
|
||||
document.addEventListener('visibilitychange', userActivityDetected); |
||||
|
||||
// check every 2 seconds
|
||||
setInterval(checkForInActiveUser, 2000); |
||||
|
||||
// handle document clicks that should hide things
|
||||
body.click((evt) => { |
||||
const target = $(evt.target); |
||||
if (target.parents().length === 0) { |
||||
return; |
||||
} |
||||
|
||||
// ensure dropdown menu doesn't impact on z-index
|
||||
body.find('.dropdown-menu-open').removeClass('dropdown-menu-open'); |
||||
|
||||
// for stuff that animates, slides out etc, clicking it needs to
|
||||
// hide it right away
|
||||
const clickAutoHide = target.closest('[data-click-hide]'); |
||||
if (clickAutoHide.length) { |
||||
const clickAutoHideParent = clickAutoHide.parent(); |
||||
clickAutoHide.detach(); |
||||
setTimeout(() => { |
||||
clickAutoHideParent.append(clickAutoHide); |
||||
}, 100); |
||||
} |
||||
|
||||
// hide popovers
|
||||
const popover = elem.find('.popover'); |
||||
if (popover.length > 0 && target.parents('.graph-legend').length === 0) { |
||||
popover.hide(); |
||||
} |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('grafanaApp', grafanaAppDirective); |
@ -1,153 +0,0 @@ |
||||
import { |
||||
ClipboardButton, |
||||
ColorPicker, |
||||
DataLinksInlineEditor, |
||||
DataSourceHttpSettings, |
||||
GraphContextMenu, |
||||
Icon, |
||||
LegacyForms, |
||||
SeriesColorPickerPopoverWithTheme, |
||||
Spinner, |
||||
UnitPicker, |
||||
} from '@grafana/ui'; |
||||
import { react2AngularDirective } from 'app/angular/react2angular'; |
||||
import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; |
||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings'; |
||||
import { QueryEditor as CloudMonitoringQueryEditor } from 'app/plugins/datasource/cloud-monitoring/components/QueryEditor'; |
||||
|
||||
import EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA'; |
||||
import { Footer } from '../core/components/Footer/Footer'; |
||||
import { MetricSelect } from '../core/components/Select/MetricSelect'; |
||||
import { TagFilter } from '../core/components/TagFilter/TagFilter'; |
||||
import { HelpModal } from '../core/components/help/HelpModal'; |
||||
|
||||
import { PageHeader } from './components/PageHeader/PageHeader'; |
||||
|
||||
const { SecretFormField } = LegacyForms; |
||||
|
||||
export function registerAngularDirectives() { |
||||
react2AngularDirective('footer', Footer, []); |
||||
react2AngularDirective('icon', Icon, [ |
||||
'name', |
||||
'size', |
||||
'type', |
||||
['onClick', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
react2AngularDirective('spinner', Spinner, ['inline']); |
||||
react2AngularDirective('helpModal', HelpModal, []); |
||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); |
||||
react2AngularDirective('emptyListCta', EmptyListCTA, [ |
||||
'title', |
||||
'buttonIcon', |
||||
'buttonLink', |
||||
'buttonTitle', |
||||
['onClick', { watchDepth: 'reference', wrapApply: true }], |
||||
'proTip', |
||||
'proTipLink', |
||||
'proTipLinkTitle', |
||||
'proTipTarget', |
||||
'infoBox', |
||||
'infoBoxTitle', |
||||
]); |
||||
react2AngularDirective('tagFilter', TagFilter, [ |
||||
'tags', |
||||
['onChange', { watchDepth: 'reference' }], |
||||
['tagOptions', { watchDepth: 'reference' }], |
||||
]); |
||||
react2AngularDirective('colorPicker', ColorPicker, [ |
||||
'color', |
||||
['onChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopoverWithTheme, [ |
||||
'color', |
||||
'series', |
||||
'onColorChange', |
||||
'onToggleAxis', |
||||
]); |
||||
react2AngularDirective('unitPicker', UnitPicker, [ |
||||
'value', |
||||
'width', |
||||
['onChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
react2AngularDirective('metricSelect', MetricSelect, [ |
||||
'options', |
||||
'onChange', |
||||
'value', |
||||
'isSearchable', |
||||
'className', |
||||
'placeholder', |
||||
['variables', { watchDepth: 'reference' }], |
||||
]); |
||||
react2AngularDirective('cloudMonitoringQueryEditor', CloudMonitoringQueryEditor, [ |
||||
'target', |
||||
'onQueryChange', |
||||
'onExecuteQuery', |
||||
['events', { watchDepth: 'reference' }], |
||||
['datasource', { watchDepth: 'reference' }], |
||||
['templateSrv', { watchDepth: 'reference' }], |
||||
]); |
||||
react2AngularDirective('secretFormField', SecretFormField, [ |
||||
'value', |
||||
'isConfigured', |
||||
'inputWidth', |
||||
'labelWidth', |
||||
'aria-label', |
||||
['onReset', { watchDepth: 'reference', wrapApply: true }], |
||||
['onChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
react2AngularDirective('graphContextMenu', GraphContextMenu, [ |
||||
'x', |
||||
'y', |
||||
'itemsGroup', |
||||
['onClose', { watchDepth: 'reference', wrapApply: true }], |
||||
['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }], |
||||
['timeZone', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
|
||||
// We keep the drilldown terminology here because of as using data-* directive
|
||||
// being in conflict with HTML data attributes
|
||||
react2AngularDirective('drilldownLinksEditor', DataLinksInlineEditor, [ |
||||
'value', |
||||
'links', |
||||
'suggestions', |
||||
['onChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
|
||||
react2AngularDirective('datasourceHttpSettingsNext', DataSourceHttpSettings, [ |
||||
'defaultUrl', |
||||
'showAccessOptions', |
||||
'dataSourceConfig', |
||||
'showForwardOAuthIdentityOption', |
||||
['onChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
react2AngularDirective('folderPicker', OldFolderPicker, [ |
||||
'labelClass', |
||||
'rootName', |
||||
'enableCreateNew', |
||||
'enableReset', |
||||
'initialTitle', |
||||
'initialFolderId', |
||||
'dashboardId', |
||||
'onCreateFolder', |
||||
['enterFolderCreation', { watchDepth: 'reference', wrapApply: true }], |
||||
['exitFolderCreation', { watchDepth: 'reference', wrapApply: true }], |
||||
['onLoad', { watchDepth: 'reference', wrapApply: true }], |
||||
['onChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
|
||||
react2AngularDirective('timePickerSettings', TimePickerSettings, [ |
||||
'renderCount', |
||||
'refreshIntervals', |
||||
'timePickerHidden', |
||||
'nowDelay', |
||||
'timezone', |
||||
['onTimeZoneChange', { watchDepth: 'reference', wrapApply: true }], |
||||
['onRefreshIntervalChange', { watchDepth: 'reference', wrapApply: true }], |
||||
['onNowDelayChange', { watchDepth: 'reference', wrapApply: true }], |
||||
['onHideTimePickerChange', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
|
||||
react2AngularDirective('clipboardButton', ClipboardButton, [ |
||||
['getText', { watchDepth: 'reference', wrapApply: true }], |
||||
]); |
||||
} |
@ -1,30 +0,0 @@ |
||||
import { isArray } from 'lodash'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
export function arrayJoin() { |
||||
'use strict'; |
||||
|
||||
return { |
||||
restrict: 'A', |
||||
require: 'ngModel', |
||||
link: (scope: any, element: any, attr: any, ngModel: any) => { |
||||
function split_array(text: string) { |
||||
return (text || '').split(','); |
||||
} |
||||
|
||||
function join_array(text: string) { |
||||
if (isArray(text)) { |
||||
return ((text || '') as any).join(','); |
||||
} else { |
||||
return text; |
||||
} |
||||
} |
||||
|
||||
ngModel.$parsers.push(split_array); |
||||
ngModel.$formatters.push(join_array); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('arrayJoin', arrayJoin); |
@ -1,49 +0,0 @@ |
||||
import { ILocationService } from 'angular'; |
||||
|
||||
import { RouteParamsProvider } from '../core/navigation/patch/RouteParamsProvider'; |
||||
import { RouteProvider } from '../core/navigation/patch/RouteProvider'; |
||||
|
||||
import { AngularLocationWrapper } from './AngularLocationWrapper'; |
||||
import { coreModule } from './core_module'; |
||||
|
||||
// Neutralizing Angular’s location tampering
|
||||
// https://stackoverflow.com/a/19825756
|
||||
const tamperAngularLocation = () => { |
||||
coreModule.config([ |
||||
'$provide', |
||||
($provide: any) => { |
||||
$provide.decorator('$browser', [ |
||||
'$delegate', |
||||
($delegate: any) => { |
||||
$delegate.onUrlChange = () => {}; |
||||
$delegate.url = () => ''; |
||||
|
||||
return $delegate; |
||||
}, |
||||
]); |
||||
}, |
||||
]); |
||||
}; |
||||
|
||||
// Intercepting $location service with implementation based on history
|
||||
const interceptAngularLocation = () => { |
||||
coreModule.config([ |
||||
'$provide', |
||||
($provide: any) => { |
||||
$provide.decorator('$location', [ |
||||
'$delegate', |
||||
($delegate: ILocationService) => { |
||||
$delegate = new AngularLocationWrapper() as unknown as ILocationService; |
||||
return $delegate; |
||||
}, |
||||
]); |
||||
}, |
||||
]); |
||||
coreModule.provider('$route', RouteProvider); |
||||
coreModule.provider('$routeParams', RouteParamsProvider); |
||||
}; |
||||
|
||||
export function initAngularRoutingBridge() { |
||||
tamperAngularLocation(); |
||||
interceptAngularLocation(); |
||||
} |
@ -1,59 +0,0 @@ |
||||
import angular from 'angular'; |
||||
import $ from 'jquery'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
coreModule.directive('bsTooltip', [ |
||||
'$parse', |
||||
'$compile', |
||||
function ($parse: any, $compile: any) { |
||||
return { |
||||
restrict: 'A', |
||||
scope: true, |
||||
link: function postLink(scope: any, element: any, attrs: any) { |
||||
let getter = $parse(attrs.bsTooltip), |
||||
value = getter(scope); |
||||
scope.$watch(attrs.bsTooltip, function (newValue: any, oldValue: any) { |
||||
if (newValue !== oldValue) { |
||||
value = newValue; |
||||
} |
||||
}); |
||||
// Grafana change, always hide other tooltips
|
||||
if (true) { |
||||
element.on('show', function (ev: any) { |
||||
$('.tooltip.in').each(function () { |
||||
const $this = $(this), |
||||
tooltip = $this.data('tooltip'); |
||||
if (tooltip && !tooltip.$element.is(element)) { |
||||
$this.tooltip('hide'); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
element.tooltip({ |
||||
title: function () { |
||||
return angular.isFunction(value) ? value.apply(null, arguments) : value; |
||||
}, |
||||
html: true, |
||||
container: 'body', // Grafana change
|
||||
}); |
||||
const tooltip = element.data('tooltip'); |
||||
tooltip.show = function () { |
||||
const r = $.fn.tooltip.Constructor.prototype.show.apply(this, arguments); |
||||
this.tip().data('tooltip', this); |
||||
return r; |
||||
}; |
||||
scope._tooltip = function (event: any) { |
||||
element.tooltip(event); |
||||
}; |
||||
scope.hide = function () { |
||||
element.tooltip('hide'); |
||||
}; |
||||
scope.show = function () { |
||||
element.tooltip('show'); |
||||
}; |
||||
scope.dismiss = scope.hide; |
||||
}, |
||||
}; |
||||
}, |
||||
]); |
@ -1,63 +0,0 @@ |
||||
import angular from 'angular'; |
||||
import $ from 'jquery'; |
||||
import { isFunction } from 'lodash'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
coreModule.directive('bsTypeahead', [ |
||||
'$parse', |
||||
function ($parse: any) { |
||||
return { |
||||
restrict: 'A', |
||||
require: '?ngModel', |
||||
link: function postLink(scope: any, element: any, attrs: any, controller: any) { |
||||
let getter = $parse(attrs.bsTypeahead), |
||||
value = getter(scope); |
||||
scope.$watch(attrs.bsTypeahead, function (newValue: any, oldValue: any) { |
||||
if (newValue !== oldValue) { |
||||
value = newValue; |
||||
} |
||||
}); |
||||
element.attr('data-provide', 'typeahead'); |
||||
element.typeahead({ |
||||
source: function () { |
||||
return angular.isFunction(value) ? value.apply(null, arguments) : value; |
||||
}, |
||||
minLength: attrs.minLength || 1, |
||||
items: attrs.item, |
||||
updater: function (value: any) { |
||||
if (controller) { |
||||
scope.$apply(function () { |
||||
controller.$setViewValue(value); |
||||
}); |
||||
} |
||||
scope.$emit('typeahead-updated', value); |
||||
return value; |
||||
}, |
||||
}); |
||||
const typeahead = element.data('typeahead'); |
||||
typeahead.lookup = function () { |
||||
let items; |
||||
this.query = this.$element.val() || ''; |
||||
if (this.query.length < this.options.minLength) { |
||||
return this.shown ? this.hide() : this; |
||||
} |
||||
items = isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source; |
||||
return items ? this.process(items) : this; |
||||
}; |
||||
if (!!attrs.matchAll) { |
||||
typeahead.matcher = function () { |
||||
return true; |
||||
}; |
||||
} |
||||
if (attrs.minLength === '0') { |
||||
setTimeout(function () { |
||||
element.on('focus', function () { |
||||
element.val().length === 0 && setTimeout(element.typeahead.bind(element, 'lookup'), 200); |
||||
}); |
||||
}); |
||||
} |
||||
}, |
||||
}; |
||||
}, |
||||
]); |
@ -1,22 +0,0 @@ |
||||
import { coreModule } from 'app/angular/core_module'; |
||||
|
||||
coreModule.directive('datasourceHttpSettings', () => { |
||||
return { |
||||
scope: { |
||||
current: '=', |
||||
suggestUrl: '@', |
||||
noDirectAccess: '@', |
||||
showForwardOAuthIdentityOption: '@', |
||||
}, |
||||
templateUrl: 'public/app/angular/partials/http_settings_next.html', |
||||
link: { |
||||
pre: ($scope: any) => { |
||||
// do not show access option if direct access is disabled
|
||||
$scope.showAccessOption = $scope.noDirectAccess !== 'true'; |
||||
$scope.onChange = (datasourceSetting: any) => { |
||||
$scope.current = datasourceSetting; |
||||
}; |
||||
}, |
||||
}, |
||||
}; |
||||
}); |
@ -1,23 +0,0 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
|
||||
import { NavModelItem } from '@grafana/data'; |
||||
|
||||
import { PageHeader } from './PageHeader'; |
||||
|
||||
describe('PageHeader', () => { |
||||
describe('when the nav tree has a node with a title', () => { |
||||
it('should render the title', async () => { |
||||
const nav: NavModelItem = { |
||||
icon: 'folder-open', |
||||
id: 'node', |
||||
subTitle: 'node subtitle', |
||||
url: '', |
||||
text: 'node', |
||||
}; |
||||
|
||||
render(<PageHeader navItem={nav} />); |
||||
|
||||
expect(screen.getByRole('heading', { name: 'node' })).toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
}); |
@ -1,165 +0,0 @@ |
||||
import { css, cx } from '@emotion/css'; |
||||
import * as React from 'react'; |
||||
|
||||
import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; |
||||
import { Tab, TabsBar, Icon, useStyles2, toIconName } from '@grafana/ui'; |
||||
|
||||
import { PageInfoItem } from '../../../core/components/Page/types'; |
||||
import { PageInfo } from '../../../core/components/PageInfo/PageInfo'; |
||||
import { ProBadge } from '../../../core/components/Upgrade/ProBadge'; |
||||
|
||||
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; |
||||
|
||||
export interface Props { |
||||
navItem: NavModelItem; |
||||
renderTitle?: (title: string) => React.ReactNode; |
||||
actions?: React.ReactNode; |
||||
info?: PageInfoItem[]; |
||||
subTitle?: React.ReactNode; |
||||
} |
||||
|
||||
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => { |
||||
if (!children || children.length === 0) { |
||||
return null; |
||||
} |
||||
|
||||
const defaultSelectedItem = children.find((navItem) => { |
||||
return navItem.active === true; |
||||
}); |
||||
|
||||
return ( |
||||
<div className={`gf-form-select-wrapper width-20 ${customCss}`}> |
||||
<div className="dropdown"> |
||||
<button |
||||
type="button" |
||||
className="gf-form-input dropdown-toggle" |
||||
data-toggle="dropdown" |
||||
style={{ textAlign: 'left' }} |
||||
> |
||||
{defaultSelectedItem?.text} |
||||
</button> |
||||
<ul role="menu" className="dropdown-menu dropdown-menu--menu"> |
||||
{children.map((navItem: NavModelItem) => { |
||||
if (navItem.hideFromTabs) { |
||||
// TODO: Rename hideFromTabs => hideFromNav
|
||||
return null; |
||||
} |
||||
return ( |
||||
<PanelHeaderMenuItem |
||||
key={navItem.url} |
||||
iconClassName={navItem.icon} |
||||
text={navItem.text} |
||||
href={navItem.url} |
||||
/> |
||||
); |
||||
})} |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const Navigation = ({ children }: { children: NavModelItem[] }) => { |
||||
if (!children || children.length === 0) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<nav> |
||||
<SelectNav customCss="page-header__select-nav">{children}</SelectNav> |
||||
<TabsBar className="page-header__tabs" hideBorder={true}> |
||||
{children.map((child, index) => { |
||||
return ( |
||||
!child.hideFromTabs && ( |
||||
<Tab |
||||
label={child.text} |
||||
active={child.active} |
||||
key={`${child.url}-${index}`} |
||||
icon={child.icon} |
||||
href={child.url} |
||||
suffix={child.tabSuffix} |
||||
/> |
||||
) |
||||
); |
||||
})} |
||||
</TabsBar> |
||||
</nav> |
||||
); |
||||
}; |
||||
|
||||
export const PageHeader = ({ navItem: model, renderTitle, actions, info, subTitle }: Props) => { |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
if (!model) { |
||||
return null; |
||||
} |
||||
|
||||
const renderHeader = (main: NavModelItem) => { |
||||
const marginTop = main.icon === 'grafana' ? 12 : 14; |
||||
const icon = main.icon && toIconName(main.icon); |
||||
const sub = subTitle ?? main.subTitle; |
||||
|
||||
return ( |
||||
<div className="page-header__inner"> |
||||
<span className="page-header__logo"> |
||||
{icon && <Icon name={icon} size="xxxl" style={{ marginTop }} />} |
||||
{main.img && <img className="page-header__img" src={main.img} alt="" />} |
||||
</span> |
||||
|
||||
<div className={cx('page-header__info-block', styles.headerText)}> |
||||
{renderTitle ? renderTitle(main.text) : renderHeaderTitle(main.text, main.highlightText)} |
||||
{info && <PageInfo info={info} />} |
||||
{sub && <div className="page-header__sub-title">{sub}</div>} |
||||
{actions && <div className={styles.actions}>{actions}</div>} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
return ( |
||||
<div className={styles.headerCanvas}> |
||||
<div className="page-container"> |
||||
<div className="page-header"> |
||||
{renderHeader(model)} |
||||
{model.children && model.children.length > 0 && <Navigation>{model.children}</Navigation>} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
function renderHeaderTitle(title: string, highlightText: NavModelItem['highlightText']) { |
||||
if (!title) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<h1 className="page-header__title"> |
||||
{title} |
||||
{highlightText && ( |
||||
<ProBadge |
||||
text={highlightText} |
||||
className={css({ |
||||
verticalAlign: 'middle', |
||||
})} |
||||
/> |
||||
)} |
||||
</h1> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
actions: css({ |
||||
display: 'flex', |
||||
flexDirection: 'row', |
||||
gap: theme.spacing(1), |
||||
}), |
||||
headerText: css({ |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
gap: theme.spacing(1), |
||||
}), |
||||
headerCanvas: css({ |
||||
background: theme.colors.background.canvas, |
||||
}), |
||||
}); |
@ -1,96 +0,0 @@ |
||||
import { css } from '@emotion/css'; |
||||
import { useState } from 'react'; |
||||
import * as React from 'react'; |
||||
|
||||
import { PanelMenuItem, GrafanaTheme2 } from '@grafana/data'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
import { Icon, toIconName, useStyles2 } from '@grafana/ui'; |
||||
|
||||
interface Props { |
||||
children?: React.ReactNode; |
||||
} |
||||
|
||||
export const PanelHeaderMenuItem = (props: Props & PanelMenuItem) => { |
||||
const [ref, setRef] = useState<HTMLLIElement | null>(null); |
||||
const isSubMenu = props.type === 'submenu'; |
||||
const styles = useStyles2(getStyles); |
||||
const icon = props.iconClassName ? toIconName(props.iconClassName) : undefined; |
||||
|
||||
switch (props.type) { |
||||
case 'divider': |
||||
return <li className="divider" />; |
||||
case 'group': |
||||
return ( |
||||
<li> |
||||
<span className={styles.groupLabel}>{props.text}</span> |
||||
</li> |
||||
); |
||||
default: |
||||
return ( |
||||
<li |
||||
className={isSubMenu ? `dropdown-submenu ${getDropdownLocationCssClass(ref)}` : undefined} |
||||
ref={setRef} |
||||
data-testid={selectors.components.Panels.Panel.menuItems(props.text)} |
||||
> |
||||
<a onClick={props.onClick} href={props.href} role="menuitem"> |
||||
{icon && <Icon name={icon} className={styles.menuIconClassName} />} |
||||
<span |
||||
className="dropdown-item-text" |
||||
data-testid={selectors.components.Panels.Panel.headerItems(props.text)} |
||||
> |
||||
{props.text} |
||||
{isSubMenu && <Icon name="angle-right" className={styles.shortcutIconClassName} />} |
||||
</span> |
||||
|
||||
{props.shortcut && ( |
||||
<span className="dropdown-menu-item-shortcut"> |
||||
<Icon name="keyboard" className={styles.menuIconClassName} /> {props.shortcut} |
||||
</span> |
||||
)} |
||||
</a> |
||||
{props.children} |
||||
</li> |
||||
); |
||||
} |
||||
}; |
||||
|
||||
function getDropdownLocationCssClass(element: HTMLElement | null) { |
||||
if (!element) { |
||||
return 'invisible'; |
||||
} |
||||
|
||||
const wrapperPos = element.parentElement!.getBoundingClientRect(); |
||||
const pos = element.getBoundingClientRect(); |
||||
|
||||
if (pos.width === 0) { |
||||
return 'invisible'; |
||||
} |
||||
|
||||
if (wrapperPos.right + pos.width + 10 > window.innerWidth) { |
||||
return 'pull-left'; |
||||
} else { |
||||
return 'pull-right'; |
||||
} |
||||
} |
||||
|
||||
function getStyles(theme: GrafanaTheme2) { |
||||
return { |
||||
menuIconClassName: css({ |
||||
marginRight: theme.spacing(1), |
||||
'a::after': { |
||||
display: 'none', |
||||
}, |
||||
}), |
||||
shortcutIconClassName: css({ |
||||
position: 'absolute', |
||||
top: '7px', |
||||
right: theme.spacing(0.5), |
||||
color: theme.colors.text.secondary, |
||||
}), |
||||
groupLabel: css({ |
||||
color: theme.colors.text.secondary, |
||||
fontSize: theme.typography.size.sm, |
||||
padding: theme.spacing(0.5, 1), |
||||
}), |
||||
}; |
||||
} |
@ -1,10 +0,0 @@ |
||||
import { coreModule } from 'app/angular/core_module'; |
||||
|
||||
coreModule.directive('datasourceTlsAuthSettings', () => { |
||||
return { |
||||
scope: { |
||||
current: '=', |
||||
}, |
||||
templateUrl: 'public/app/angular/partials/tls_auth_settings.html', |
||||
}; |
||||
}); |
@ -1,4 +0,0 @@ |
||||
declare module 'brace/*' { |
||||
let brace: any; |
||||
export default brace; |
||||
} |
@ -1,200 +0,0 @@ |
||||
/** |
||||
* codeEditor directive based on Ace code editor |
||||
* https://github.com/ajaxorg/ace
|
||||
* |
||||
* Basic usage: |
||||
* <code-editor content="ctrl.target.query" on-change="ctrl.panelCtrl.refresh()" |
||||
* data-mode="sql" data-show-gutter> |
||||
* </code-editor> |
||||
* |
||||
* Params: |
||||
* content: Editor content. |
||||
* onChange: Function called on content change (invoked on editor blur, ctrl+enter, not on every change). |
||||
* getCompleter: Function returned external completer. Completer is an object implemented getCompletions() method, |
||||
* see Prometheus Data Source implementation for details. |
||||
* |
||||
* Some Ace editor options available via data-* attributes: |
||||
* data-mode - Language mode (text, sql, javascript, etc.). Default is 'text'. |
||||
* data-theme - Editor theme (eg 'solarized_dark'). |
||||
* data-max-lines - Max editor height in lines. Editor grows automatically from 1 to maxLines. |
||||
* data-show-gutter - Show gutter (contains line numbers and additional info). |
||||
* data-tab-size - Tab size, default is 2. |
||||
* data-behaviours-enabled - Specifies whether to use behaviors or not. "Behaviors" in this case is the auto-pairing of |
||||
* special characters, like quotation marks, parenthesis, or brackets. |
||||
* data-snippets-enabled - Specifies whether to use snippets or not. "Snippets" are small pieces of code that can be |
||||
* inserted via the completion box. |
||||
* |
||||
* Keybindings: |
||||
* Ctrl-Enter (Command-Enter): run onChange() function |
||||
*/ |
||||
|
||||
import coreModule from 'app/angular/core_module'; |
||||
import config from 'app/core/config'; |
||||
|
||||
const DEFAULT_THEME_DARK = 'ace/theme/grafana-dark'; |
||||
const DEFAULT_THEME_LIGHT = 'ace/theme/textmate'; |
||||
const DEFAULT_MODE = 'text'; |
||||
const DEFAULT_MAX_LINES = 10; |
||||
const DEFAULT_TAB_SIZE = 2; |
||||
const DEFAULT_BEHAVIORS = true; |
||||
const DEFAULT_SNIPPETS = true; |
||||
|
||||
const editorTemplate = `<div></div>`; |
||||
|
||||
async function link(scope: any, elem: any, attrs: any) { |
||||
// Options
|
||||
const langMode = attrs.mode || DEFAULT_MODE; |
||||
const maxLines = attrs.maxLines || DEFAULT_MAX_LINES; |
||||
const showGutter = attrs.showGutter !== undefined; |
||||
const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; |
||||
const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIORS; |
||||
const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; |
||||
|
||||
// Initialize editor
|
||||
const aceElem = elem.get(0); |
||||
const { default: ace } = await import(/* webpackChunkName: "brace" */ 'brace'); |
||||
await import('brace/ext/language_tools'); |
||||
await import('brace/theme/textmate'); |
||||
await import('brace/mode/text'); |
||||
await import('brace/snippets/text'); |
||||
await import('brace/mode/sql'); |
||||
await import('brace/snippets/sql'); |
||||
await import('brace/mode/sqlserver'); |
||||
await import('brace/snippets/sqlserver'); |
||||
await import('brace/mode/markdown'); |
||||
await import('brace/snippets/markdown'); |
||||
await import('brace/mode/json'); |
||||
await import('brace/snippets/json'); |
||||
|
||||
// @ts-ignore
|
||||
await import('./theme-grafana-dark'); |
||||
|
||||
const codeEditor = ace.edit(aceElem); |
||||
const editorSession = codeEditor.getSession(); |
||||
|
||||
const editorOptions = { |
||||
maxLines: maxLines, |
||||
showGutter: showGutter, |
||||
tabSize: tabSize, |
||||
behavioursEnabled: behavioursEnabled, |
||||
highlightActiveLine: false, |
||||
showPrintMargin: false, |
||||
autoScrollEditorIntoView: true, // this is needed if editor is inside scrollable page
|
||||
}; |
||||
|
||||
// Set options
|
||||
codeEditor.setOptions(editorOptions); |
||||
// disable depreacation warning
|
||||
codeEditor.$blockScrolling = Infinity; |
||||
// Padding hacks
|
||||
(codeEditor.renderer as any).setScrollMargin(10, 10); |
||||
codeEditor.renderer.setPadding(10); |
||||
|
||||
setThemeMode(); |
||||
setLangMode(langMode); |
||||
setEditorContent(scope.content); |
||||
|
||||
// Add classes
|
||||
elem.addClass('gf-code-editor'); |
||||
const textarea = elem.find('textarea'); |
||||
textarea.addClass('gf-form-input'); |
||||
|
||||
// All aria-label to be set for accessibility
|
||||
textarea.attr('aria-label', attrs.textareaLabel); |
||||
|
||||
if (scope.codeEditorFocus) { |
||||
setTimeout(() => { |
||||
textarea.focus(); |
||||
const domEl = textarea[0]; |
||||
if (domEl.setSelectionRange) { |
||||
const pos = textarea.val().length * 2; |
||||
domEl.setSelectionRange(pos, pos); |
||||
} |
||||
}, 100); |
||||
} |
||||
|
||||
// Event handlers
|
||||
editorSession.on('change', (e) => { |
||||
scope.$apply(() => { |
||||
const newValue = codeEditor.getValue(); |
||||
scope.content = newValue; |
||||
}); |
||||
}); |
||||
|
||||
// Sync with outer scope - update editor content if model has been changed from outside of directive.
|
||||
scope.$watch('content', (newValue: any, oldValue: any) => { |
||||
const editorValue = codeEditor.getValue(); |
||||
if (newValue !== editorValue && newValue !== oldValue) { |
||||
scope.$$postDigest(() => { |
||||
setEditorContent(newValue); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
codeEditor.on('blur', () => { |
||||
scope.onChange(); |
||||
}); |
||||
|
||||
scope.$on('$destroy', () => { |
||||
codeEditor.destroy(); |
||||
}); |
||||
|
||||
// Keybindings
|
||||
codeEditor.commands.addCommand({ |
||||
name: 'executeQuery', |
||||
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, |
||||
exec: () => { |
||||
scope.onChange(); |
||||
}, |
||||
}); |
||||
|
||||
function setLangMode(lang: string) { |
||||
ace.acequire('ace/ext/language_tools'); |
||||
codeEditor.setOptions({ |
||||
enableBasicAutocompletion: true, |
||||
enableLiveAutocompletion: true, |
||||
enableSnippets: snippetsEnabled, |
||||
}); |
||||
|
||||
if (scope.getCompleter()) { |
||||
// make copy of array as ace seems to share completers array between instances
|
||||
const anyEditor = codeEditor as any; |
||||
anyEditor.completers = anyEditor.completers.slice(); |
||||
anyEditor.completers.push(scope.getCompleter()); |
||||
} |
||||
|
||||
const aceModeName = `ace/mode/${lang}`; |
||||
editorSession.setMode(aceModeName); |
||||
} |
||||
|
||||
function setThemeMode() { |
||||
let theme = DEFAULT_THEME_DARK; |
||||
if (config.bootData.user.lightTheme) { |
||||
theme = DEFAULT_THEME_LIGHT; |
||||
} |
||||
|
||||
codeEditor.setTheme(theme); |
||||
} |
||||
|
||||
function setEditorContent(value: string) { |
||||
codeEditor.setValue(value); |
||||
codeEditor.clearSelection(); |
||||
} |
||||
} |
||||
|
||||
export function codeEditorDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
template: editorTemplate, |
||||
scope: { |
||||
content: '=', |
||||
datasource: '=', |
||||
codeEditorFocus: '<', |
||||
onChange: '&', |
||||
getCompleter: '&', |
||||
}, |
||||
link: link, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('codeEditor', codeEditorDirective); |
@ -1,117 +0,0 @@ |
||||
ace.define( |
||||
'ace/theme/grafana-dark', |
||||
['require', 'exports', 'module', 'ace/lib/dom'], |
||||
function (acequire, exports, module) { |
||||
'use strict'; |
||||
|
||||
exports.isDark = true; |
||||
exports.cssClass = 'gf-code-dark'; |
||||
exports.cssText = |
||||
'.gf-code-dark .ace_gutter {\ |
||||
background: #2f3129;\ |
||||
color: #8f908a\ |
||||
}\ |
||||
.gf-code-dark .ace_print-margin {\ |
||||
width: 1px;\ |
||||
background: #555651\ |
||||
}\ |
||||
.gf-code-dark {\ |
||||
background-color: #09090b;\ |
||||
color: #e0e0e0\ |
||||
}\ |
||||
.gf-code-dark .ace_cursor {\ |
||||
color: #f8f8f0\ |
||||
}\ |
||||
.gf-code-dark .ace_marker-layer .ace_selection {\ |
||||
background: #49483e\ |
||||
}\ |
||||
.gf-code-dark.ace_multiselect .ace_selection.ace_start {\ |
||||
box-shadow: 0 0 3px 0px #272822;\ |
||||
}\ |
||||
.gf-code-dark .ace_marker-layer .ace_step {\ |
||||
background: rgb(102, 82, 0)\ |
||||
}\ |
||||
.gf-code-dark .ace_marker-layer .ace_bracket {\ |
||||
margin: -1px 0 0 -1px;\ |
||||
border: 1px solid #49483e\ |
||||
}\ |
||||
.gf-code-dark .ace_marker-layer .ace_active-line {\ |
||||
background: #202020\ |
||||
}\ |
||||
.gf-code-dark .ace_gutter-active-line {\ |
||||
background-color: #272727\ |
||||
}\ |
||||
.gf-code-dark .ace_marker-layer .ace_selected-word {\ |
||||
border: 1px solid #49483e\ |
||||
}\ |
||||
.gf-code-dark .ace_invisible {\ |
||||
color: #52524d\ |
||||
}\ |
||||
.gf-code-dark .ace_entity.ace_name.ace_tag,\ |
||||
.gf-code-dark .ace_keyword,\ |
||||
.gf-code-dark .ace_meta.ace_tag,\ |
||||
.gf-code-dark .ace_storage {\ |
||||
color: #66d9ef\ |
||||
}\ |
||||
.gf-code-dark .ace_punctuation,\ |
||||
.gf-code-dark .ace_punctuation.ace_tag {\ |
||||
color: #fff\ |
||||
}\ |
||||
.gf-code-dark .ace_constant.ace_character,\ |
||||
.gf-code-dark .ace_constant.ace_language,\ |
||||
.gf-code-dark .ace_constant.ace_numeric,\ |
||||
.gf-code-dark .ace_constant.ace_other {\ |
||||
color: #fe85fc\ |
||||
}\ |
||||
.gf-code-dark .ace_invalid {\ |
||||
color: #f8f8f0;\ |
||||
background-color: #f92672\ |
||||
}\ |
||||
.gf-code-dark .ace_invalid.ace_deprecated {\ |
||||
color: #f8f8f0;\ |
||||
background-color: #ae81ff\ |
||||
}\ |
||||
.gf-code-dark .ace_support.ace_constant,\ |
||||
.gf-code-dark .ace_support.ace_function {\ |
||||
color: #59e6e3\ |
||||
}\ |
||||
.gf-code-dark .ace_fold {\ |
||||
background-color: #a6e22e;\ |
||||
border-color: #f8f8f2\ |
||||
}\ |
||||
.gf-code-dark .ace_storage.ace_type,\ |
||||
.gf-code-dark .ace_support.ace_class,\ |
||||
.gf-code-dark .ace_support.ace_type {\ |
||||
font-style: italic;\ |
||||
color: #66d9ef\ |
||||
}\ |
||||
.gf-code-dark .ace_entity.ace_name.ace_function,\ |
||||
.gf-code-dark .ace_entity.ace_other,\ |
||||
.gf-code-dark .ace_entity.ace_other.ace_attribute-name,\ |
||||
.gf-code-dark .ace_variable {\ |
||||
color: #a6e22e\ |
||||
}\ |
||||
.gf-code-dark .ace_variable.ace_parameter {\ |
||||
font-style: italic;\ |
||||
color: #fd971f\ |
||||
}\ |
||||
.gf-code-dark .ace_string {\ |
||||
color: #74e680\ |
||||
}\ |
||||
.gf-code-dark .ace_paren {\ |
||||
color: #f0a842\ |
||||
}\ |
||||
.gf-code-dark .ace_operator {\ |
||||
color: #FFF\ |
||||
}\ |
||||
.gf-code-dark .ace_comment {\ |
||||
color: #75715e\ |
||||
}\ |
||||
.gf-code-dark .ace_indent-guide {\ |
||||
background: url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaaaccayaaaczgbynaaaaekleqvqimwpq0fd0zxbzd/wpaajvaoxesgneaaaaaelftksuqmcc) right repeat-y\ |
||||
}'; |
||||
|
||||
const dom = acequire('../lib/dom'); |
||||
dom.importCssString(exports.cssText, exports.cssClass); |
||||
} |
||||
); |
@ -1,286 +0,0 @@ |
||||
import { ISCEService } from 'angular'; |
||||
import { debounce, find, indexOf, map, isObject, escape, unescape } from 'lodash'; |
||||
|
||||
import coreModule from '../../core_module'; |
||||
import { promiseToDigest } from '../../promiseToDigest'; |
||||
|
||||
function typeaheadMatcher(this: any, item: string) { |
||||
let str = this.query; |
||||
if (str === '') { |
||||
return true; |
||||
} |
||||
if (str[0] === '/') { |
||||
str = str.substring(1); |
||||
} |
||||
if (str[str.length - 1] === '/') { |
||||
str = str.substring(0, str.length - 1); |
||||
} |
||||
return item.toLowerCase().match(str.toLowerCase()); |
||||
} |
||||
|
||||
export class FormDropdownCtrl { |
||||
inputElement: JQLite; |
||||
linkElement: JQLite; |
||||
model: any; |
||||
display: any; |
||||
text: any; |
||||
options: any; |
||||
cssClass: any; |
||||
cssClasses: any; |
||||
allowCustom: any; |
||||
labelMode: boolean; |
||||
linkMode: boolean; |
||||
cancelBlur: any; |
||||
onChange: any; |
||||
getOptions: any; |
||||
optionCache: any; |
||||
lookupText: boolean; |
||||
placeholder: any; |
||||
startOpen: any; |
||||
debounce: boolean; |
||||
|
||||
static $inject = ['$scope', '$element', '$sce', 'templateSrv']; |
||||
|
||||
constructor( |
||||
private $scope: any, |
||||
$element: JQLite, |
||||
private $sce: ISCEService, |
||||
private templateSrv: any |
||||
) { |
||||
this.inputElement = $element.find('input').first(); |
||||
this.linkElement = $element.find('a').first(); |
||||
this.linkMode = true; |
||||
this.cancelBlur = null; |
||||
this.labelMode = false; |
||||
this.lookupText = false; |
||||
this.debounce = false; |
||||
|
||||
// listen to model changes
|
||||
$scope.$watch('ctrl.model', this.modelChanged.bind(this)); |
||||
} |
||||
|
||||
$onInit() { |
||||
if (this.labelMode) { |
||||
this.cssClasses = 'gf-form-label ' + this.cssClass; |
||||
} else { |
||||
this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; |
||||
} |
||||
|
||||
if (this.placeholder) { |
||||
this.inputElement.attr('placeholder', this.placeholder); |
||||
} |
||||
|
||||
this.inputElement.attr('data-provide', 'typeahead'); |
||||
this.inputElement.typeahead({ |
||||
source: this.typeaheadSource.bind(this), |
||||
minLength: 0, |
||||
items: 10000, |
||||
updater: this.typeaheadUpdater.bind(this), |
||||
matcher: typeaheadMatcher, |
||||
}); |
||||
|
||||
// modify typeahead lookup
|
||||
// this = typeahead
|
||||
const typeahead = this.inputElement.data('typeahead'); |
||||
typeahead.lookup = function () { |
||||
this.query = this.$element.val() || ''; |
||||
this.source(this.query, this.process.bind(this)); |
||||
}; |
||||
|
||||
if (this.debounce) { |
||||
typeahead.lookup = debounce(typeahead.lookup, 500, { leading: true }); |
||||
} |
||||
|
||||
this.linkElement.keydown((evt) => { |
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) { |
||||
this.linkElement.click(); |
||||
} |
||||
}); |
||||
|
||||
this.inputElement.keydown((evt) => { |
||||
if (evt.keyCode === 13) { |
||||
setTimeout(() => { |
||||
this.inputElement.blur(); |
||||
}, 300); |
||||
} |
||||
}); |
||||
|
||||
this.inputElement.blur(this.inputBlur.bind(this)); |
||||
|
||||
if (this.startOpen) { |
||||
setTimeout(this.open.bind(this), 0); |
||||
} |
||||
} |
||||
|
||||
getOptionsInternal(query: string) { |
||||
return promiseToDigest(this.$scope)(Promise.resolve(this.getOptions({ $query: query }))); |
||||
} |
||||
|
||||
isPromiseLike(obj: any) { |
||||
return obj && typeof obj.then === 'function'; |
||||
} |
||||
|
||||
modelChanged() { |
||||
if (isObject(this.model)) { |
||||
this.updateDisplay((this.model as any).text); |
||||
} else { |
||||
// if we have text use it
|
||||
if (this.lookupText) { |
||||
this.getOptionsInternal('').then((options: any) => { |
||||
const item: any = find(options, { value: this.model }); |
||||
this.updateDisplay(item ? item.text : this.model); |
||||
}); |
||||
} else { |
||||
this.updateDisplay(this.model); |
||||
} |
||||
} |
||||
} |
||||
|
||||
typeaheadSource(query: string, callback: (res: any) => void) { |
||||
this.getOptionsInternal(query).then((options: any) => { |
||||
this.optionCache = options; |
||||
|
||||
// extract texts
|
||||
const optionTexts = map(options, (op: any) => { |
||||
return escape(op.text); |
||||
}); |
||||
|
||||
// add custom values
|
||||
if (this.allowCustom && this.text !== '') { |
||||
if (indexOf(optionTexts, this.text) === -1) { |
||||
optionTexts.unshift(this.text); |
||||
} |
||||
} |
||||
|
||||
callback(optionTexts); |
||||
}); |
||||
} |
||||
|
||||
typeaheadUpdater(text: string) { |
||||
if (text === this.text) { |
||||
clearTimeout(this.cancelBlur); |
||||
this.inputElement.focus(); |
||||
return text; |
||||
} |
||||
|
||||
this.inputElement.val(text); |
||||
this.switchToLink(true); |
||||
return text; |
||||
} |
||||
|
||||
switchToLink(fromClick: boolean) { |
||||
if (this.linkMode && !fromClick) { |
||||
return; |
||||
} |
||||
|
||||
clearTimeout(this.cancelBlur); |
||||
this.cancelBlur = null; |
||||
this.linkMode = true; |
||||
this.inputElement.hide(); |
||||
this.linkElement.show(); |
||||
this.updateValue(this.inputElement.val() as string); |
||||
} |
||||
|
||||
inputBlur() { |
||||
// happens long before the click event on the typeahead options
|
||||
// need to have long delay because the blur
|
||||
this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200); |
||||
} |
||||
|
||||
updateValue(text: string) { |
||||
text = unescape(text); |
||||
|
||||
if (text === '' || this.text === text) { |
||||
return; |
||||
} |
||||
|
||||
this.$scope.$apply(() => { |
||||
const option: any = find(this.optionCache, { text: text }); |
||||
|
||||
if (option) { |
||||
if (isObject(this.model)) { |
||||
this.model = option; |
||||
} else { |
||||
this.model = option.value; |
||||
} |
||||
this.text = option.text; |
||||
} else if (this.allowCustom) { |
||||
if (isObject(this.model)) { |
||||
(this.model as any).text = (this.model as any).value = text; |
||||
} else { |
||||
this.model = text; |
||||
} |
||||
this.text = text; |
||||
} |
||||
|
||||
// needs to call this after digest so
|
||||
// property is synced with outerscope
|
||||
this.$scope.$$postDigest(() => { |
||||
this.$scope.$apply(() => { |
||||
this.onChange({ $option: option }); |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
updateDisplay(text: string) { |
||||
this.text = text; |
||||
this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); |
||||
} |
||||
|
||||
open() { |
||||
this.inputElement.css('width', Math.max(this.linkElement.width()!, 80) + 16 + 'px'); |
||||
|
||||
this.inputElement.show(); |
||||
this.inputElement.focus(); |
||||
|
||||
this.linkElement.hide(); |
||||
this.linkMode = false; |
||||
|
||||
const typeahead = this.inputElement.data('typeahead'); |
||||
if (typeahead) { |
||||
this.inputElement.val(''); |
||||
typeahead.lookup(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const template = ` |
||||
<input type="text" |
||||
data-provide="typeahead" |
||||
class="gf-form-input" |
||||
spellcheck="false" |
||||
style="display:none"> |
||||
</input> |
||||
<a ng-class="ctrl.cssClasses" |
||||
tabindex="1" |
||||
ng-click="ctrl.open()" |
||||
give-focus="ctrl.focus" |
||||
ng-bind-html="ctrl.display || ' '"> |
||||
</a> |
||||
`;
|
||||
|
||||
export function formDropdownDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
template: template, |
||||
controller: FormDropdownCtrl, |
||||
bindToController: true, |
||||
controllerAs: 'ctrl', |
||||
scope: { |
||||
model: '=', |
||||
getOptions: '&', |
||||
onChange: '&', |
||||
cssClass: '@', |
||||
allowCustom: '@', |
||||
labelMode: '@', |
||||
lookupText: '@', |
||||
placeholder: '@', |
||||
startOpen: '@', |
||||
debounce: '@', |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('gfFormDropdown', formDropdownDirective); |
@ -1,69 +0,0 @@ |
||||
import { each } from 'lodash'; |
||||
// @ts-ignore
|
||||
import Drop from 'tether-drop'; |
||||
|
||||
import coreModule from 'app/angular/core_module'; |
||||
|
||||
export function infoPopover() { |
||||
return { |
||||
restrict: 'E', |
||||
template: `<icon name="'info-circle'" style="margin-left: 10px;" size="'xs'"></icon>`, |
||||
transclude: true, |
||||
link: (scope: any, elem: any, attrs: any, ctrl: any, transclude: any) => { |
||||
const offset = attrs.offset || '0 -10px'; |
||||
const position = attrs.position || 'right middle'; |
||||
let classes = 'drop-help drop-hide-out-of-bounds'; |
||||
const openOn = 'hover'; |
||||
|
||||
elem.addClass('gf-form-help-icon'); |
||||
|
||||
if (attrs.wide) { |
||||
classes += ' drop-wide'; |
||||
} |
||||
|
||||
if (attrs.mode) { |
||||
elem.addClass('gf-form-help-icon--' + attrs.mode); |
||||
} |
||||
|
||||
transclude((clone: any, newScope: any) => { |
||||
const content = document.createElement('div'); |
||||
content.className = 'markdown-html'; |
||||
|
||||
each(clone, (node) => { |
||||
content.appendChild(node); |
||||
}); |
||||
|
||||
const dropOptions = { |
||||
target: elem[0], |
||||
content: content, |
||||
position: position, |
||||
classes: classes, |
||||
openOn: openOn, |
||||
hoverOpenDelay: 400, |
||||
tetherOptions: { |
||||
offset: offset, |
||||
constraints: [ |
||||
{ |
||||
to: 'window', |
||||
attachment: 'together', |
||||
pin: true, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
// Create drop in next digest after directive content is rendered.
|
||||
scope.$applyAsync(() => { |
||||
const drop = new Drop(dropOptions); |
||||
|
||||
const unbind = scope.$on('$destroy', () => { |
||||
drop.destroy(); |
||||
unbind(); |
||||
}); |
||||
}); |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('infoPopover', infoPopover); |
@ -1,29 +0,0 @@ |
||||
import { JsonExplorer } from '@grafana/ui'; |
||||
import coreModule from 'app/angular/core_module'; |
||||
|
||||
coreModule.directive('jsonTree', [ |
||||
function jsonTreeDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
scope: { |
||||
object: '=', |
||||
startExpanded: '@', |
||||
rootName: '@', |
||||
}, |
||||
link: (scope: any, elem) => { |
||||
let expansionLevel = scope.startExpanded; |
||||
if (scope.startExpanded === 'true') { |
||||
expansionLevel = 2; |
||||
} else if (scope.startExpanded === 'false') { |
||||
expansionLevel = 1; |
||||
} |
||||
const jsonObject = { [scope.rootName]: scope.object }; |
||||
const jsonExp = new JsonExplorer(jsonObject, expansionLevel, { |
||||
animateOpen: true, |
||||
}); |
||||
const html = jsonExp.render(true); |
||||
elem.append(html); |
||||
}, |
||||
}; |
||||
}, |
||||
]); |
@ -1,249 +0,0 @@ |
||||
import angular, { ILocationService } from 'angular'; |
||||
import { each } from 'lodash'; |
||||
|
||||
import { DataSourceApi, PanelEvents } from '@grafana/data'; |
||||
import coreModule from 'app/angular/core_module'; |
||||
import config from 'app/core/config'; |
||||
|
||||
import { importPanelPlugin } from '../../features/plugins/importPanelPlugin'; |
||||
import { importDataSourcePlugin, importAppPlugin } from '../../features/plugins/plugin_loader'; |
||||
|
||||
coreModule.directive('pluginComponent', ['$compile', '$http', '$templateCache', '$location', pluginDirectiveLoader]); |
||||
|
||||
function pluginDirectiveLoader($compile: any, $http: any, $templateCache: any, $location: ILocationService) { |
||||
function getTemplate(component: { template: any; templateUrl: any }) { |
||||
if (component.template) { |
||||
return Promise.resolve(component.template); |
||||
} |
||||
const cached = $templateCache.get(component.templateUrl); |
||||
if (cached) { |
||||
return Promise.resolve(cached); |
||||
} |
||||
return $http.get(component.templateUrl).then((res: any) => { |
||||
return res.data; |
||||
}); |
||||
} |
||||
|
||||
function relativeTemplateUrlToAbs(templateUrl: string, baseUrl: string) { |
||||
if (!templateUrl) { |
||||
return undefined; |
||||
} |
||||
if (templateUrl.indexOf('public') === 0) { |
||||
return templateUrl; |
||||
} |
||||
|
||||
return baseUrl + '/' + templateUrl; |
||||
} |
||||
|
||||
function getPluginComponentDirective(options: any) { |
||||
// handle relative template urls for plugin templates
|
||||
options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl); |
||||
|
||||
return () => { |
||||
return { |
||||
templateUrl: options.Component.templateUrl, |
||||
template: options.Component.template, |
||||
restrict: 'E', |
||||
controller: options.Component, |
||||
controllerAs: 'ctrl', |
||||
bindToController: true, |
||||
scope: options.bindings, |
||||
link: (scope: any, elem: any, attrs: any, ctrl: any) => { |
||||
if (ctrl.link) { |
||||
ctrl.link(scope, elem, attrs, ctrl); |
||||
} |
||||
if (ctrl.init) { |
||||
ctrl.init(); |
||||
} |
||||
}, |
||||
}; |
||||
}; |
||||
} |
||||
|
||||
function loadPanelComponentInfo(scope: any, attrs: any) { |
||||
const componentInfo: any = { |
||||
name: 'panel-plugin-' + scope.panel.type, |
||||
bindings: { dashboard: '=', panel: '=', row: '=' }, |
||||
attrs: { |
||||
dashboard: 'dashboard', |
||||
panel: 'panel', |
||||
class: 'panel-height-helper', |
||||
}, |
||||
}; |
||||
|
||||
const panelInfo = config.panels[scope.panel.type]; |
||||
return importPanelPlugin(panelInfo.id).then((panelPlugin) => { |
||||
const PanelCtrl = panelPlugin.angularPanelCtrl; |
||||
componentInfo.Component = PanelCtrl; |
||||
|
||||
if (!PanelCtrl || PanelCtrl.registered) { |
||||
return componentInfo; |
||||
} |
||||
|
||||
if (PanelCtrl.templatePromise) { |
||||
return PanelCtrl.templatePromise.then((res: any) => { |
||||
return componentInfo; |
||||
}); |
||||
} |
||||
|
||||
if (panelInfo) { |
||||
PanelCtrl.templateUrl = relativeTemplateUrlToAbs(PanelCtrl.templateUrl, panelInfo.baseUrl); |
||||
} |
||||
|
||||
PanelCtrl.templatePromise = getTemplate(PanelCtrl).then((template: any) => { |
||||
PanelCtrl.templateUrl = null; |
||||
PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-height-helper">${template}</grafana-panel>`; |
||||
return { ...componentInfo, baseUrl: panelInfo.baseUrl }; |
||||
}); |
||||
|
||||
return PanelCtrl.templatePromise; |
||||
}); |
||||
} |
||||
|
||||
function getModule(scope: any, attrs: any): any { |
||||
switch (attrs.type) { |
||||
// QueryCtrl
|
||||
case 'query-ctrl': { |
||||
const ds: DataSourceApi = scope.ctrl.datasource as DataSourceApi; |
||||
|
||||
return Promise.resolve({ |
||||
baseUrl: ds.meta.baseUrl, |
||||
name: 'query-ctrl-' + ds.meta.id, |
||||
bindings: { target: '=', panelCtrl: '=', datasource: '=' }, |
||||
attrs: { |
||||
target: 'ctrl.target', |
||||
'panel-ctrl': 'ctrl', |
||||
datasource: 'ctrl.datasource', |
||||
}, |
||||
Component: ds.components!.QueryCtrl, |
||||
}); |
||||
} |
||||
// Annotations
|
||||
case 'annotations-query-ctrl': { |
||||
const baseUrl = scope.ctrl.currentDatasource.meta.baseUrl; |
||||
const pluginId = scope.ctrl.currentDatasource.meta.id; |
||||
|
||||
return importDataSourcePlugin(scope.ctrl.currentDatasource.meta).then((dsPlugin) => { |
||||
return { |
||||
baseUrl, |
||||
name: 'annotations-query-ctrl-' + pluginId, |
||||
bindings: { annotation: '=', datasource: '=' }, |
||||
attrs: { |
||||
annotation: 'ctrl.currentAnnotation', |
||||
datasource: 'ctrl.currentDatasource', |
||||
}, |
||||
Component: dsPlugin.components.AnnotationsQueryCtrl, |
||||
}; |
||||
}); |
||||
} |
||||
// Datasource ConfigCtrl
|
||||
case 'datasource-config-ctrl': { |
||||
const dsMeta = scope.ctrl.datasourceMeta; |
||||
const angularUrl = $location.url(); |
||||
return importDataSourcePlugin(dsMeta).then((dsPlugin) => { |
||||
scope.$watch( |
||||
'ctrl.current', |
||||
() => { |
||||
// This watcher can trigger when we navigate away due to late digests
|
||||
// This check is to stop onModelChanged from being called when navigating away
|
||||
// as it triggers a redux action which comes before the angular $routeChangeSucces and
|
||||
// This makes the bridgeSrv think location changed from redux before detecting it was actually
|
||||
// changed from angular.
|
||||
if (angularUrl === $location.url()) { |
||||
scope.onModelChanged(scope.ctrl.current); |
||||
} |
||||
}, |
||||
true |
||||
); |
||||
|
||||
return { |
||||
baseUrl: dsMeta.baseUrl, |
||||
name: 'ds-config-' + dsMeta.id, |
||||
bindings: { meta: '=', current: '=' }, |
||||
attrs: { meta: 'ctrl.datasourceMeta', current: 'ctrl.current' }, |
||||
Component: dsPlugin.angularConfigCtrl, |
||||
}; |
||||
}); |
||||
} |
||||
// AppConfigCtrl
|
||||
case 'app-config-ctrl': { |
||||
const model = scope.ctrl.model; |
||||
return importAppPlugin(model).then((appPlugin) => { |
||||
return { |
||||
baseUrl: model.baseUrl, |
||||
name: 'app-config-' + model.id, |
||||
bindings: { appModel: '=', appEditCtrl: '=' }, |
||||
attrs: { 'app-model': 'ctrl.model', 'app-edit-ctrl': 'ctrl' }, |
||||
Component: appPlugin.angularConfigCtrl, |
||||
}; |
||||
}); |
||||
} |
||||
// Panel
|
||||
case 'panel': { |
||||
return loadPanelComponentInfo(scope, attrs); |
||||
} |
||||
default: { |
||||
return Promise.reject({ |
||||
message: 'Could not find component type: ' + attrs.type, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function appendAndCompile(scope: any, elem: JQuery, componentInfo: any) { |
||||
const child = angular.element(document.createElement(componentInfo.name)); |
||||
each(componentInfo.attrs, (value, key) => { |
||||
child.attr(key, value); |
||||
}); |
||||
|
||||
$compile(child)(scope); |
||||
elem.empty(); |
||||
|
||||
// let a binding digest cycle complete before adding to dom
|
||||
setTimeout(() => { |
||||
scope.$applyAsync(() => { |
||||
elem.append(child); |
||||
setTimeout(() => { |
||||
scope.$applyAsync(() => { |
||||
scope.$broadcast(PanelEvents.componentDidMount.name); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
function registerPluginComponent(scope: any, elem: JQuery, attrs: any, componentInfo: any) { |
||||
if (componentInfo.notFound) { |
||||
elem.empty(); |
||||
return; |
||||
} |
||||
|
||||
if (!componentInfo.Component) { |
||||
throw { |
||||
message: 'Failed to find exported plugin component for ' + componentInfo.name, |
||||
}; |
||||
} |
||||
|
||||
if (!componentInfo.Component.registered) { |
||||
const directiveName = attrs.$normalize(componentInfo.name); |
||||
const directiveFn = getPluginComponentDirective(componentInfo); |
||||
coreModule.directive(directiveName, directiveFn); |
||||
componentInfo.Component.registered = true; |
||||
} |
||||
|
||||
appendAndCompile(scope, elem, componentInfo); |
||||
} |
||||
|
||||
return { |
||||
restrict: 'E', |
||||
link: (scope: any, elem: JQuery, attrs: any) => { |
||||
getModule(scope, attrs) |
||||
.then((componentInfo: any) => { |
||||
registerPluginComponent(scope, elem, attrs, componentInfo); |
||||
}) |
||||
.catch((err: any) => { |
||||
console.error('Plugin component error', err); |
||||
}); |
||||
}, |
||||
}; |
||||
} |
@ -1,186 +0,0 @@ |
||||
import $ from 'jquery'; |
||||
import { debounce, each, map, partial, escape, unescape } from 'lodash'; |
||||
|
||||
import coreModule from 'app/angular/core_module'; |
||||
|
||||
import { promiseToDigest } from '../promiseToDigest'; |
||||
|
||||
const template = ` |
||||
<div class="dropdown cascade-open"> |
||||
<a ng-click="showActionsMenu()" class="query-part-name pointer dropdown-toggle" data-toggle="dropdown">{{part.def.type}}</a> |
||||
<span>(</span><span class="query-part-parameters"></span><span>)</span> |
||||
<ul class="dropdown-menu"> |
||||
<li ng-repeat="action in partActions"> |
||||
<a ng-click="triggerPartAction(action)">{{action.text}}</a> |
||||
</li> |
||||
</ul> |
||||
`;
|
||||
|
||||
coreModule.directive('queryPartEditor', ['templateSrv', queryPartEditorDirective]); |
||||
|
||||
export function queryPartEditorDirective(templateSrv: any) { |
||||
const paramTemplate = '<input type="text" class="hide input-mini tight-form-func-param"></input>'; |
||||
|
||||
return { |
||||
restrict: 'E', |
||||
template: template, |
||||
scope: { |
||||
part: '=', |
||||
handleEvent: '&', |
||||
debounce: '@', |
||||
}, |
||||
link: function postLink($scope: any, elem: any) { |
||||
const part = $scope.part; |
||||
const partDef = part.def; |
||||
const $paramsContainer = elem.find('.query-part-parameters'); |
||||
const debounceLookup = $scope.debounce; |
||||
|
||||
$scope.partActions = []; |
||||
|
||||
function clickFuncParam(this: any, paramIndex: number) { |
||||
const $link = $(this); |
||||
const $input = $link.next(); |
||||
|
||||
$input.val(part.params[paramIndex]); |
||||
$input.css('width', $link.width()! + 16 + 'px'); |
||||
|
||||
$link.hide(); |
||||
$input.show(); |
||||
$input.focus(); |
||||
$input.select(); |
||||
|
||||
const typeahead = $input.data('typeahead'); |
||||
if (typeahead) { |
||||
$input.val(''); |
||||
typeahead.lookup(); |
||||
} |
||||
} |
||||
|
||||
function inputBlur(this: any, paramIndex: number) { |
||||
const $input = $(this); |
||||
const $link = $input.prev(); |
||||
const newValue = $input.val(); |
||||
|
||||
if (newValue !== '' || part.def.params[paramIndex].optional) { |
||||
$link.html(templateSrv.highlightVariablesAsHtml(newValue)); |
||||
|
||||
part.updateParam($input.val(), paramIndex); |
||||
$scope.$apply(() => { |
||||
$scope.handleEvent({ $event: { name: 'part-param-changed' } }); |
||||
}); |
||||
} |
||||
|
||||
$input.hide(); |
||||
$link.show(); |
||||
} |
||||
|
||||
function inputKeyPress(this: any, paramIndex: number, e: any) { |
||||
if (e.which === 13) { |
||||
inputBlur.call(this, paramIndex); |
||||
} |
||||
} |
||||
|
||||
function inputKeyDown(this: any) { |
||||
this.style.width = (3 + this.value.length) * 8 + 'px'; |
||||
} |
||||
|
||||
function addTypeahead($input: JQuery, param: any, paramIndex: number) { |
||||
if (!param.options && !param.dynamicLookup) { |
||||
return; |
||||
} |
||||
|
||||
const typeaheadSource = (query: string, callback: any) => { |
||||
if (param.options) { |
||||
let options = param.options; |
||||
if (param.type === 'int') { |
||||
options = map(options, (val) => { |
||||
return val.toString(); |
||||
}); |
||||
} |
||||
return options; |
||||
} |
||||
|
||||
$scope.$apply(() => { |
||||
$scope.handleEvent({ $event: { name: 'get-param-options' } }).then((result: any) => { |
||||
const dynamicOptions = map(result, (op) => { |
||||
return escape(op.value); |
||||
}); |
||||
callback(dynamicOptions); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
$input.attr('data-provide', 'typeahead'); |
||||
|
||||
$input.typeahead({ |
||||
source: typeaheadSource, |
||||
minLength: 0, |
||||
items: 1000, |
||||
updater: (value: string) => { |
||||
value = unescape(value); |
||||
setTimeout(() => { |
||||
inputBlur.call($input[0], paramIndex); |
||||
}, 0); |
||||
return value; |
||||
}, |
||||
}); |
||||
|
||||
const typeahead = $input.data('typeahead'); |
||||
typeahead.lookup = function () { |
||||
this.query = this.$element.val() || ''; |
||||
const items = this.source(this.query, $.proxy(this.process, this)); |
||||
return items ? this.process(items) : items; |
||||
}; |
||||
|
||||
if (debounceLookup) { |
||||
typeahead.lookup = debounce(typeahead.lookup, 500, { leading: true }); |
||||
} |
||||
} |
||||
|
||||
$scope.showActionsMenu = () => { |
||||
promiseToDigest($scope)( |
||||
$scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => { |
||||
$scope.partActions = res; |
||||
}) |
||||
); |
||||
}; |
||||
|
||||
$scope.triggerPartAction = (action: string) => { |
||||
$scope.handleEvent({ $event: { name: 'action', action: action } }); |
||||
}; |
||||
|
||||
function addElementsAndCompile() { |
||||
each(partDef.params, (param: any, index: number) => { |
||||
if (param.optional && part.params.length <= index) { |
||||
return; |
||||
} |
||||
|
||||
if (index > 0) { |
||||
$('<span>, </span>').appendTo($paramsContainer); |
||||
} |
||||
|
||||
const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); |
||||
const $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>'); |
||||
const $input = $(paramTemplate); |
||||
|
||||
$paramLink.appendTo($paramsContainer); |
||||
$input.appendTo($paramsContainer); |
||||
|
||||
$input.blur(partial(inputBlur, index)); |
||||
$input.keyup(inputKeyDown); |
||||
$input.keypress(partial(inputKeyPress, index)); |
||||
$paramLink.click(partial(clickFuncParam, index)); |
||||
|
||||
addTypeahead($input, param, index); |
||||
}); |
||||
} |
||||
|
||||
function relink() { |
||||
$paramsContainer.empty(); |
||||
addElementsAndCompile(); |
||||
} |
||||
|
||||
relink(); |
||||
}, |
||||
}; |
||||
} |
@ -1,50 +0,0 @@ |
||||
// @ts-ignore
|
||||
import baron from 'baron'; |
||||
import $ from 'jquery'; |
||||
|
||||
import coreModule from 'app/angular/core_module'; |
||||
|
||||
const scrollBarHTML = ` |
||||
<div class="baron__track"> |
||||
<div class="baron__bar"></div> |
||||
</div> |
||||
`;
|
||||
|
||||
const scrollRootClass = 'baron baron__root'; |
||||
const scrollerClass = 'baron__scroller'; |
||||
|
||||
export function geminiScrollbar() { |
||||
return { |
||||
restrict: 'A', |
||||
link: (scope: any, elem: any, attrs: any) => { |
||||
let scrollRoot = elem.parent(); |
||||
const scroller = elem; |
||||
|
||||
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { |
||||
scrollRoot = scroller; |
||||
} |
||||
|
||||
scrollRoot.addClass(scrollRootClass); |
||||
$(scrollBarHTML).appendTo(scrollRoot); |
||||
elem.addClass(scrollerClass); |
||||
|
||||
const scrollParams = { |
||||
root: scrollRoot[0], |
||||
scroller: scroller[0], |
||||
bar: '.baron__bar', |
||||
barOnCls: '_scrollbar', |
||||
scrollingCls: '_scrolling', |
||||
track: '.baron__track', |
||||
direction: 'v', |
||||
}; |
||||
|
||||
const scrollbar = baron(scrollParams); |
||||
|
||||
scope.$on('$destroy', () => { |
||||
scrollbar.dispose(); |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('grafanaScrollbar', geminiScrollbar); |
@ -1,24 +0,0 @@ |
||||
/** |
||||
* Wrapper for the new ngReact <color-picker> directive for backward compatibility. |
||||
* Allows remaining <spectrum-picker> untouched in outdated plugins. |
||||
* Technically, it's just a wrapper for react component with two-way data binding support. |
||||
*/ |
||||
import coreModule from '../core_module'; |
||||
|
||||
coreModule.directive('spectrumPicker', spectrumPicker); |
||||
|
||||
export function spectrumPicker() { |
||||
return { |
||||
restrict: 'E', |
||||
require: 'ngModel', |
||||
scope: true, |
||||
replace: true, |
||||
template: '<color-picker color="ngModel.$viewValue" on-change="onColorChange"></color-picker>', |
||||
link: (scope: any, element: any, attrs: any, ngModel: any) => { |
||||
scope.ngModel = ngModel; |
||||
scope.onColorChange = (color: string) => { |
||||
ngModel.$setViewValue(color); |
||||
}; |
||||
}, |
||||
}; |
||||
} |
@ -1,74 +0,0 @@ |
||||
import { clone } from 'lodash'; |
||||
|
||||
export class SqlPartDef { |
||||
type: string; |
||||
style: string; |
||||
label: string; |
||||
params: any[]; |
||||
defaultParams: any[]; |
||||
wrapOpen: string; |
||||
wrapClose: string; |
||||
separator: string; |
||||
|
||||
constructor(options: any) { |
||||
this.type = options.type; |
||||
if (options.label) { |
||||
this.label = options.label; |
||||
} else { |
||||
this.label = this.type[0].toUpperCase() + this.type.substring(1) + ':'; |
||||
} |
||||
this.style = options.style; |
||||
if (this.style === 'function') { |
||||
this.wrapOpen = '('; |
||||
this.wrapClose = ')'; |
||||
this.separator = ', '; |
||||
} else { |
||||
this.wrapOpen = ' '; |
||||
this.wrapClose = ' '; |
||||
this.separator = ' '; |
||||
} |
||||
this.params = options.params; |
||||
this.defaultParams = options.defaultParams; |
||||
} |
||||
} |
||||
|
||||
export class SqlPart { |
||||
part: any; |
||||
def: SqlPartDef; |
||||
params: any[]; |
||||
label: string; |
||||
name: string; |
||||
datatype: string; |
||||
|
||||
constructor(part: any, def: any) { |
||||
this.part = part; |
||||
this.def = def; |
||||
if (!this.def) { |
||||
throw { message: 'Could not find sql part ' + part.type }; |
||||
} |
||||
|
||||
this.datatype = part.datatype; |
||||
|
||||
if (part.name) { |
||||
this.name = part.name; |
||||
this.label = def.label + ' ' + part.name; |
||||
} else { |
||||
this.name = ''; |
||||
this.label = def.label; |
||||
} |
||||
|
||||
part.params = part.params || clone(this.def.defaultParams); |
||||
this.params = part.params; |
||||
} |
||||
|
||||
updateParam(strValue: string, index: number) { |
||||
// handle optional parameters
|
||||
if (strValue === '' && this.def.params[index].optional) { |
||||
this.params.splice(index, 1); |
||||
} else { |
||||
this.params[index] = strValue; |
||||
} |
||||
|
||||
this.part.params = this.params; |
||||
} |
||||
} |
@ -1,196 +0,0 @@ |
||||
import $ from 'jquery'; |
||||
import { debounce, each, indexOf, map, partial, escape, unescape } from 'lodash'; |
||||
|
||||
import coreModule from 'app/angular/core_module'; |
||||
|
||||
const template = ` |
||||
<div class="dropdown cascade-open"> |
||||
<a ng-click="showActionsMenu()" class="query-part-name pointer dropdown-toggle" data-toggle="dropdown">{{part.label}}</a> |
||||
<span>{{part.def.wrapOpen}}</span><span class="query-part-parameters"></span><span>{{part.def.wrapClose}}</span> |
||||
<ul class="dropdown-menu"> |
||||
<li ng-repeat="action in partActions"> |
||||
<a ng-click="triggerPartAction(action)">{{action.text}}</a> |
||||
</li> |
||||
</ul> |
||||
`;
|
||||
|
||||
coreModule.directive('sqlPartEditor', ['templateSrv', sqlPartEditorDirective]); |
||||
|
||||
export function sqlPartEditorDirective(templateSrv: any) { |
||||
const paramTemplate = '<input type="text" class="hide input-mini"></input>'; |
||||
|
||||
return { |
||||
restrict: 'E', |
||||
template: template, |
||||
scope: { |
||||
part: '=', |
||||
handleEvent: '&', |
||||
debounce: '@', |
||||
}, |
||||
link: function postLink($scope: any, elem: any) { |
||||
const part = $scope.part; |
||||
const partDef = part.def; |
||||
const $paramsContainer = elem.find('.query-part-parameters'); |
||||
const debounceLookup = $scope.debounce; |
||||
let cancelBlur: any = null; |
||||
|
||||
$scope.partActions = []; |
||||
|
||||
function clickFuncParam(this: any, paramIndex: number) { |
||||
const $link = $(this); |
||||
const $input = $link.next(); |
||||
|
||||
$input.val(part.params[paramIndex]); |
||||
$input.css('width', $link.width()! + 16 + 'px'); |
||||
|
||||
$link.hide(); |
||||
$input.show(); |
||||
$input.focus(); |
||||
$input.select(); |
||||
|
||||
const typeahead = $input.data('typeahead'); |
||||
if (typeahead) { |
||||
$input.val(''); |
||||
typeahead.lookup(); |
||||
} |
||||
} |
||||
|
||||
function inputBlur($input: JQuery, paramIndex: number) { |
||||
cancelBlur = setTimeout(() => { |
||||
switchToLink($input, paramIndex); |
||||
}, 200); |
||||
} |
||||
|
||||
function switchToLink($input: JQuery, paramIndex: number) { |
||||
const $link = $input.prev(); |
||||
const newValue = $input.val(); |
||||
|
||||
if (newValue !== '' || part.def.params[paramIndex].optional) { |
||||
$link.html(templateSrv.highlightVariablesAsHtml(newValue)); |
||||
|
||||
part.updateParam($input.val(), paramIndex); |
||||
$scope.$apply(() => { |
||||
$scope.handleEvent({ $event: { name: 'part-param-changed' } }); |
||||
}); |
||||
} |
||||
|
||||
$input.hide(); |
||||
$link.show(); |
||||
} |
||||
|
||||
function inputKeyPress(this: any, paramIndex: number, e: any) { |
||||
if (e.which === 13) { |
||||
switchToLink($(this), paramIndex); |
||||
} |
||||
} |
||||
|
||||
function inputKeyDown(this: any) { |
||||
this.style.width = (3 + this.value.length) * 8 + 'px'; |
||||
} |
||||
|
||||
function addTypeahead($input: JQuery, param: any, paramIndex: number) { |
||||
if (!param.options && !param.dynamicLookup) { |
||||
return; |
||||
} |
||||
|
||||
const typeaheadSource = (query: string, callback: any) => { |
||||
if (param.options) { |
||||
let options = param.options; |
||||
if (param.type === 'int') { |
||||
options = map(options, (val) => { |
||||
return val.toString(); |
||||
}); |
||||
} |
||||
return options; |
||||
} |
||||
|
||||
$scope.$apply(() => { |
||||
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then((result: any) => { |
||||
const dynamicOptions = map(result, (op) => { |
||||
return escape(op.value); |
||||
}); |
||||
|
||||
// add current value to dropdown if it's not in dynamicOptions
|
||||
if (indexOf(dynamicOptions, part.params[paramIndex]) === -1) { |
||||
dynamicOptions.unshift(escape(part.params[paramIndex])); |
||||
} |
||||
|
||||
callback(dynamicOptions); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
$input.attr('data-provide', 'typeahead'); |
||||
|
||||
$input.typeahead({ |
||||
source: typeaheadSource, |
||||
minLength: 0, |
||||
items: 1000, |
||||
updater: (value: string) => { |
||||
value = unescape(value); |
||||
if (value === part.params[paramIndex]) { |
||||
clearTimeout(cancelBlur); |
||||
$input.focus(); |
||||
return value; |
||||
} |
||||
return value; |
||||
}, |
||||
}); |
||||
|
||||
const typeahead = $input.data('typeahead'); |
||||
typeahead.lookup = function () { |
||||
this.query = this.$element.val() || ''; |
||||
const items = this.source(this.query, $.proxy(this.process, this)); |
||||
return items ? this.process(items) : items; |
||||
}; |
||||
|
||||
if (debounceLookup) { |
||||
typeahead.lookup = debounce(typeahead.lookup, 500, { leading: true }); |
||||
} |
||||
} |
||||
|
||||
$scope.showActionsMenu = () => { |
||||
$scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => { |
||||
$scope.partActions = res; |
||||
}); |
||||
}; |
||||
|
||||
$scope.triggerPartAction = (action: string) => { |
||||
$scope.handleEvent({ $event: { name: 'action', action: action } }); |
||||
}; |
||||
|
||||
function addElementsAndCompile() { |
||||
each(partDef.params, (param: any, index: number) => { |
||||
if (param.optional && part.params.length <= index) { |
||||
return; |
||||
} |
||||
|
||||
if (index > 0) { |
||||
$('<span>' + partDef.separator + '</span>').appendTo($paramsContainer); |
||||
} |
||||
|
||||
const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); |
||||
const $paramLink = $('<a class="query-part__link">' + paramValue + '</a>'); |
||||
const $input = $(paramTemplate); |
||||
|
||||
$paramLink.appendTo($paramsContainer); |
||||
$input.appendTo($paramsContainer); |
||||
|
||||
$input.blur(partial(inputBlur, $input, index)); |
||||
$input.keyup(inputKeyDown); |
||||
$input.keypress(partial(inputKeyPress, index)); |
||||
$paramLink.click(partial(clickFuncParam, index)); |
||||
|
||||
addTypeahead($input, param, index); |
||||
}); |
||||
} |
||||
|
||||
function relink() { |
||||
$paramsContainer.empty(); |
||||
addElementsAndCompile(); |
||||
} |
||||
|
||||
relink(); |
||||
}, |
||||
}; |
||||
} |
@ -1,94 +0,0 @@ |
||||
import coreModule from 'app/angular/core_module'; |
||||
|
||||
const template = ` |
||||
<label for="check-{{ctrl.id}}" class="gf-form-switch-container"> |
||||
<div class="gf-form-label {{ctrl.labelClass}}" ng-show="ctrl.label"> |
||||
{{ctrl.label}} |
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center"> |
||||
{{ctrl.tooltip}} |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show"> |
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()"> |
||||
<span class="gf-form-switch__slider"></span> |
||||
</div> |
||||
</label> |
||||
`;
|
||||
|
||||
const checkboxTemplate = ` |
||||
<label for="check-{{ctrl.id}}" class="gf-form-switch-container"> |
||||
<div class="gf-form-label {{ctrl.labelClass}}" ng-show="ctrl.label"> |
||||
{{ctrl.label}} |
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center"> |
||||
{{ctrl.tooltip}} |
||||
</info-popover> |
||||
</div> |
||||
<div class="gf-form-checkbox {{ctrl.switchClass}}" ng-if="ctrl.show"> |
||||
<input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()"> |
||||
<span class="gf-form-switch__checkbox"></span> |
||||
</div> |
||||
</label> |
||||
`;
|
||||
|
||||
export class SwitchCtrl { |
||||
onChange: any; |
||||
checked: any; |
||||
show: any; |
||||
id: any; |
||||
label?: string; |
||||
|
||||
static $inject = ['$scope', '$timeout']; |
||||
|
||||
constructor( |
||||
$scope: any, |
||||
private $timeout: any |
||||
) { |
||||
this.show = true; |
||||
this.id = $scope.$id; |
||||
} |
||||
|
||||
internalOnChange() { |
||||
return this.$timeout(() => { |
||||
return this.onChange(); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export function switchDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
controller: SwitchCtrl, |
||||
controllerAs: 'ctrl', |
||||
bindToController: true, |
||||
scope: { |
||||
checked: '=', |
||||
label: '@', |
||||
labelClass: '@', |
||||
tooltip: '@', |
||||
switchClass: '@', |
||||
onChange: '&', |
||||
}, |
||||
template: template, |
||||
}; |
||||
} |
||||
|
||||
export function checkboxDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
controller: SwitchCtrl, |
||||
controllerAs: 'ctrl', |
||||
bindToController: true, |
||||
scope: { |
||||
checked: '=', |
||||
label: '@', |
||||
labelClass: '@', |
||||
tooltip: '@', |
||||
switchClass: '@', |
||||
onChange: '&', |
||||
}, |
||||
template: checkboxTemplate, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('gfFormSwitch', switchDirective); |
||||
coreModule.directive('gfFormCheckbox', checkboxDirective); |
@ -1,18 +0,0 @@ |
||||
import angular from 'angular'; |
||||
|
||||
const coreModule = angular.module('grafana.core', ['ngRoute']); |
||||
|
||||
// legacy modules
|
||||
const angularModules = [ |
||||
coreModule, |
||||
angular.module('grafana.controllers', []), |
||||
angular.module('grafana.directives', []), |
||||
angular.module('grafana.factories', []), |
||||
angular.module('grafana.services', []), |
||||
angular.module('grafana.filters', []), |
||||
angular.module('grafana.routes', []), |
||||
]; |
||||
|
||||
export { angularModules, coreModule }; |
||||
|
||||
export default coreModule; |
@ -1,80 +0,0 @@ |
||||
import angular from 'angular'; |
||||
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
export class DeltaCtrl { |
||||
observer: any; |
||||
|
||||
constructor() { |
||||
const waitForCompile = () => {}; |
||||
|
||||
this.observer = new MutationObserver(waitForCompile); |
||||
|
||||
const observerConfig = { |
||||
attributes: true, |
||||
attributeFilter: ['class'], |
||||
characterData: false, |
||||
childList: true, |
||||
subtree: false, |
||||
}; |
||||
|
||||
this.observer.observe(angular.element('.delta-html')[0], observerConfig); |
||||
} |
||||
|
||||
$onDestroy() { |
||||
this.observer.disconnect(); |
||||
} |
||||
} |
||||
|
||||
export function delta() { |
||||
return { |
||||
controller: DeltaCtrl, |
||||
replace: false, |
||||
restrict: 'A', |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('diffDelta', delta); |
||||
|
||||
// Link to JSON line number
|
||||
export class LinkJSONCtrl { |
||||
static $inject = ['$scope', '$rootScope', '$anchorScroll']; |
||||
|
||||
constructor( |
||||
private $scope: any, |
||||
private $rootScope: GrafanaRootScope, |
||||
private $anchorScroll: any |
||||
) {} |
||||
|
||||
goToLine(line: number) { |
||||
let unbind: () => void; |
||||
|
||||
const scroll = () => { |
||||
this.$anchorScroll(`l${line}`); |
||||
unbind(); |
||||
}; |
||||
|
||||
this.$scope.switchView().then(() => { |
||||
unbind = this.$rootScope.$on('json-diff-ready', scroll.bind(this)); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export function linkJson() { |
||||
return { |
||||
controller: LinkJSONCtrl, |
||||
controllerAs: 'ctrl', |
||||
replace: true, |
||||
restrict: 'E', |
||||
scope: { |
||||
line: '@lineDisplay', |
||||
link: '@lineLink', |
||||
switchView: '&', |
||||
}, |
||||
template: `<a class="diff-linenum btn btn-inverse btn-small" ng-click="ctrl.goToLine(link)">Line {{ line }}</a>`, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('diffLinkJson', linkJson); |
@ -1,273 +0,0 @@ |
||||
import $ from 'jquery'; |
||||
import { each, reduce } from 'lodash'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
export function dropdownTypeahead($compile: any) { |
||||
const inputTemplate = |
||||
'<input type="text"' + |
||||
' class="gf-form-input input-medium tight-form-input"' + |
||||
' spellcheck="false" style="display:none"></input>'; |
||||
|
||||
const buttonTemplate = |
||||
'<a class="gf-form-label tight-form-func dropdown-toggle"' + |
||||
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' + |
||||
' ><i class="fa fa-plus"></i></a>'; |
||||
|
||||
return { |
||||
scope: { |
||||
menuItems: '=dropdownTypeahead', |
||||
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect', |
||||
model: '=ngModel', |
||||
}, |
||||
link: ($scope: any, elem: any, attrs: any) => { |
||||
const $input = $(inputTemplate); |
||||
const $button = $(buttonTemplate); |
||||
$input.appendTo(elem); |
||||
$button.appendTo(elem); |
||||
|
||||
if (attrs.linkText) { |
||||
$button.html(attrs.linkText); |
||||
} |
||||
|
||||
if (attrs.ngModel) { |
||||
$scope.$watch('model', (newValue: any) => { |
||||
each($scope.menuItems, (item) => { |
||||
each(item.submenu, (subItem) => { |
||||
if (subItem.value === newValue) { |
||||
$button.html(subItem.text); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
const typeaheadValues = reduce( |
||||
$scope.menuItems, |
||||
(memo: any[], value, index) => { |
||||
if (!value.submenu) { |
||||
value.click = 'menuItemSelected(' + index + ')'; |
||||
memo.push(value.text); |
||||
} else { |
||||
each(value.submenu, (item, subIndex) => { |
||||
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; |
||||
memo.push(value.text + ' ' + item.text); |
||||
}); |
||||
} |
||||
return memo; |
||||
}, |
||||
[] |
||||
); |
||||
|
||||
const closeDropdownMenu = () => { |
||||
$input.hide(); |
||||
$input.val(''); |
||||
$button.show(); |
||||
$button.focus(); |
||||
elem.removeClass('open'); |
||||
}; |
||||
|
||||
$scope.menuItemSelected = (index: number, subIndex: number) => { |
||||
const menuItem = $scope.menuItems[index]; |
||||
const payload: any = { $item: menuItem }; |
||||
if (menuItem.submenu && subIndex !== void 0) { |
||||
payload.$subItem = menuItem.submenu[subIndex]; |
||||
} |
||||
$scope.dropdownTypeaheadOnSelect(payload); |
||||
closeDropdownMenu(); |
||||
}; |
||||
|
||||
$input.attr('data-provide', 'typeahead'); |
||||
$input.typeahead({ |
||||
source: typeaheadValues, |
||||
minLength: 1, |
||||
items: 10, |
||||
updater: (value: string) => { |
||||
const result: any = {}; |
||||
each($scope.menuItems, (menuItem) => { |
||||
each(menuItem.submenu, (submenuItem) => { |
||||
if (value === menuItem.text + ' ' + submenuItem.text) { |
||||
result.$subItem = submenuItem; |
||||
result.$item = menuItem; |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
if (result.$item) { |
||||
$scope.$apply(() => { |
||||
$scope.dropdownTypeaheadOnSelect(result); |
||||
}); |
||||
} |
||||
|
||||
$input.trigger('blur'); |
||||
return ''; |
||||
}, |
||||
}); |
||||
|
||||
$button.click(() => { |
||||
$button.hide(); |
||||
$input.show(); |
||||
$input.focus(); |
||||
}); |
||||
|
||||
$input.keyup(() => { |
||||
elem.toggleClass('open', $input.val() === ''); |
||||
}); |
||||
|
||||
elem.mousedown((evt: Event) => { |
||||
evt.preventDefault(); |
||||
}); |
||||
|
||||
$input.blur(() => { |
||||
$input.hide(); |
||||
$input.val(''); |
||||
$button.show(); |
||||
$button.focus(); |
||||
// clicking the function dropdown menu won't
|
||||
// work if you remove class at once
|
||||
setTimeout(() => { |
||||
elem.removeClass('open'); |
||||
}, 200); |
||||
}); |
||||
|
||||
$compile(elem.contents())($scope); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
export function dropdownTypeahead2($compile: any) { |
||||
const inputTemplate = |
||||
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>'; |
||||
|
||||
const buttonTemplate = |
||||
'<a class="{{buttonTemplateClass}} dropdown-toggle"' + |
||||
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' + |
||||
' ><i class="fa fa-plus"></i></a>'; |
||||
|
||||
return { |
||||
scope: { |
||||
menuItems: '=dropdownTypeahead2', |
||||
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect', |
||||
model: '=ngModel', |
||||
buttonTemplateClass: '@', |
||||
}, |
||||
link: ($scope: any, elem: any, attrs: any) => { |
||||
const $input = $(inputTemplate); |
||||
|
||||
if (!$scope.buttonTemplateClass) { |
||||
$scope.buttonTemplateClass = 'gf-form-input'; |
||||
} |
||||
|
||||
const $button = $(buttonTemplate); |
||||
const timeoutId = { |
||||
blur: null as any, |
||||
}; |
||||
$input.appendTo(elem); |
||||
$button.appendTo(elem); |
||||
|
||||
if (attrs.linkText) { |
||||
$button.html(attrs.linkText); |
||||
} |
||||
|
||||
if (attrs.ngModel) { |
||||
$scope.$watch('model', (newValue: any) => { |
||||
each($scope.menuItems, (item) => { |
||||
each(item.submenu, (subItem) => { |
||||
if (subItem.value === newValue) { |
||||
$button.html(subItem.text); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
const typeaheadValues = reduce( |
||||
$scope.menuItems, |
||||
(memo: any[], value, index) => { |
||||
if (!value.submenu) { |
||||
value.click = 'menuItemSelected(' + index + ')'; |
||||
memo.push(value.text); |
||||
} else { |
||||
each(value.submenu, (item, subIndex) => { |
||||
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; |
||||
memo.push(value.text + ' ' + item.text); |
||||
}); |
||||
} |
||||
return memo; |
||||
}, |
||||
[] |
||||
); |
||||
|
||||
const closeDropdownMenu = () => { |
||||
$input.hide(); |
||||
$input.val(''); |
||||
$button.show(); |
||||
$button.focus(); |
||||
elem.removeClass('open'); |
||||
}; |
||||
|
||||
$scope.menuItemSelected = (index: number, subIndex: number) => { |
||||
const menuItem = $scope.menuItems[index]; |
||||
const payload: any = { $item: menuItem }; |
||||
if (menuItem.submenu && subIndex !== void 0) { |
||||
payload.$subItem = menuItem.submenu[subIndex]; |
||||
} |
||||
$scope.dropdownTypeaheadOnSelect(payload); |
||||
closeDropdownMenu(); |
||||
}; |
||||
|
||||
$input.attr('data-provide', 'typeahead'); |
||||
$input.typeahead({ |
||||
source: typeaheadValues, |
||||
minLength: 1, |
||||
items: 10, |
||||
updater: (value: string) => { |
||||
const result: any = {}; |
||||
each($scope.menuItems, (menuItem) => { |
||||
each(menuItem.submenu, (submenuItem) => { |
||||
if (value === menuItem.text + ' ' + submenuItem.text) { |
||||
result.$subItem = submenuItem; |
||||
result.$item = menuItem; |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
if (result.$item) { |
||||
$scope.$apply(() => { |
||||
$scope.dropdownTypeaheadOnSelect(result); |
||||
}); |
||||
} |
||||
|
||||
$input.trigger('blur'); |
||||
return ''; |
||||
}, |
||||
}); |
||||
|
||||
$button.click(() => { |
||||
$button.hide(); |
||||
$input.show(); |
||||
$input.focus(); |
||||
}); |
||||
|
||||
$input.keyup(() => { |
||||
elem.toggleClass('open', $input.val() === ''); |
||||
}); |
||||
|
||||
elem.mousedown((evt: Event) => { |
||||
evt.preventDefault(); |
||||
timeoutId.blur = null; |
||||
}); |
||||
|
||||
$input.blur(() => { |
||||
timeoutId.blur = setTimeout(() => { |
||||
closeDropdownMenu(); |
||||
}, 1); |
||||
}); |
||||
|
||||
$compile(elem.contents())($scope); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('dropdownTypeahead', ['$compile', dropdownTypeahead]); |
||||
coreModule.directive('dropdownTypeahead2', ['$compile', dropdownTypeahead2]); |
@ -1,61 +0,0 @@ |
||||
import angular from 'angular'; |
||||
import { isArray, isNull, isObject, isUndefined } from 'lodash'; |
||||
|
||||
import { dateTime } from '@grafana/data'; |
||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; |
||||
|
||||
import coreModule from '../core_module'; |
||||
|
||||
coreModule.filter('stringSort', () => { |
||||
return (input: any) => { |
||||
return input.sort(); |
||||
}; |
||||
}); |
||||
|
||||
coreModule.filter('slice', () => { |
||||
return (arr: any[], start: any, end: any) => { |
||||
if (!isUndefined(arr)) { |
||||
return arr.slice(start, end); |
||||
} |
||||
return arr; |
||||
}; |
||||
}); |
||||
|
||||
coreModule.filter('stringify', () => { |
||||
return (arr: any[]) => { |
||||
if (isObject(arr) && !isArray(arr)) { |
||||
return angular.toJson(arr); |
||||
} else { |
||||
return isNull(arr) ? null : arr.toString(); |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
coreModule.filter('moment', () => { |
||||
return (date: string, mode: string) => { |
||||
switch (mode) { |
||||
case 'ago': |
||||
return dateTime(date).fromNow(); |
||||
} |
||||
return dateTime(date).fromNow(); |
||||
}; |
||||
}); |
||||
|
||||
function interpolateTemplateVars(templateSrv: TemplateSrv = getTemplateSrv()) { |
||||
const filterFunc: any = (text: string, scope: any) => { |
||||
let scopedVars; |
||||
if (scope.ctrl) { |
||||
scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars; |
||||
} else { |
||||
scopedVars = scope.row.scopedVars; |
||||
} |
||||
|
||||
return templateSrv.replaceWithText(text, scopedVars); |
||||
}; |
||||
|
||||
filterFunc.$stateful = true; |
||||
return filterFunc; |
||||
} |
||||
|
||||
coreModule.filter('interpolateTemplateVars', interpolateTemplateVars); |
||||
export default {}; |
@ -1,29 +0,0 @@ |
||||
import coreModule from './core_module'; |
||||
|
||||
coreModule.directive('giveFocus', () => { |
||||
return (scope: any, element: any, attrs: any) => { |
||||
element.click((e: any) => { |
||||
e.stopPropagation(); |
||||
}); |
||||
|
||||
scope.$watch( |
||||
attrs.giveFocus, |
||||
(newValue: any) => { |
||||
if (!newValue) { |
||||
return; |
||||
} |
||||
setTimeout(() => { |
||||
element.focus(); |
||||
const domEl: any = element[0]; |
||||
if (domEl.setSelectionRange) { |
||||
const pos = element.val().length * 2; |
||||
domEl.setSelectionRange(pos, pos); |
||||
} |
||||
}, 200); |
||||
}, |
||||
true |
||||
); |
||||
}; |
||||
}); |
||||
|
||||
export default {}; |
@ -1,43 +0,0 @@ |
||||
import './panel/all'; |
||||
import './partials'; |
||||
import './filters/filters'; |
||||
import './services/alert_srv'; |
||||
import './services/dynamic_directive_srv'; |
||||
import './services/ng_react'; |
||||
import './services/segment_srv'; |
||||
import './services/popover_srv'; |
||||
import './services/timer'; |
||||
import './services/AngularLoader'; |
||||
|
||||
import '../angular/jquery_extended'; |
||||
import './dropdown_typeahead'; |
||||
import './autofill_event_fix'; |
||||
import './metric_segment'; |
||||
import './misc'; |
||||
import './bsTooltip'; |
||||
import './bsTypeahead'; |
||||
import './ng_model_on_blur'; |
||||
import './tags'; |
||||
import './rebuild_on_change'; |
||||
import './give_focus'; |
||||
import './diff-view'; |
||||
import './array_join'; |
||||
import './angular_wrappers'; |
||||
|
||||
// components
|
||||
import './components/query_part_editor'; |
||||
import './components/form_dropdown/form_dropdown'; |
||||
import './components/scroll'; |
||||
import './components/jsontree'; |
||||
import './components/switch'; |
||||
import './components/info_popover'; |
||||
import './components/spectrum_picker'; |
||||
import './components/code_editor/code_editor'; |
||||
import './components/sql_part/sql_part_editor'; |
||||
import './components/HttpSettingsCtrl'; |
||||
import './components/TlsAuthSettingsCtrl'; |
||||
import './components/plugin_component'; |
||||
import './GrafanaCtrl'; |
||||
|
||||
export { AngularApp } from './AngularApp'; |
||||
export { coreModule } from './core_module'; |
@ -1,50 +0,0 @@ |
||||
export function monkeyPatchInjectorWithPreAssignedBindings(injector: any) { |
||||
injector.oldInvoke = injector.invoke; |
||||
injector.invoke = (fn: any, self: any, locals: any, serviceName: any) => { |
||||
const parentScope = locals?.$scope?.$parent; |
||||
|
||||
if (parentScope) { |
||||
// PanelCtrl
|
||||
if (parentScope.panel) { |
||||
self.panel = parentScope.panel; |
||||
} |
||||
|
||||
// Panels & dashboard SettingsCtrl
|
||||
if (parentScope.dashboard) { |
||||
self.dashboard = parentScope.dashboard; |
||||
} |
||||
|
||||
// Query editors
|
||||
if (parentScope.ctrl?.target) { |
||||
self.panelCtrl = parentScope.ctrl; |
||||
self.datasource = parentScope.ctrl.datasource; |
||||
self.target = parentScope.ctrl.target; |
||||
} |
||||
|
||||
// Data source ConfigCtrl
|
||||
if (parentScope.ctrl?.datasourceMeta) { |
||||
self.meta = parentScope.ctrl.datasourceMeta; |
||||
self.current = parentScope.ctrl.current; |
||||
} |
||||
|
||||
// Data source AnnotationsQueryCtrl
|
||||
if (parentScope.ctrl?.currentAnnotation) { |
||||
self.annotation = parentScope.ctrl.currentAnnotation; |
||||
self.datasource = parentScope.ctrl.currentDatasource; |
||||
} |
||||
|
||||
// App config ctrl
|
||||
if (parentScope.isAppConfigCtrl) { |
||||
self.appEditCtrl = parentScope.ctrl; |
||||
self.appModel = parentScope.ctrl.model; |
||||
} |
||||
|
||||
// App page ctrl
|
||||
if (parentScope.$parent?.$parent?.ctrl?.appModel) { |
||||
self.appModel = parentScope.$parent?.$parent?.ctrl?.appModel; |
||||
} |
||||
} |
||||
|
||||
return injector.oldInvoke(fn, self, locals, serviceName); |
||||
}; |
||||
} |
@ -1,52 +0,0 @@ |
||||
import angular from 'angular'; |
||||
import $ from 'jquery'; |
||||
import { extend } from 'lodash'; |
||||
|
||||
const $win = $(window); |
||||
|
||||
$.fn.place_tt = (() => { |
||||
const defaults = { |
||||
offset: 5, |
||||
}; |
||||
|
||||
return function (this: any, x: number, y: number, opts: any) { |
||||
opts = $.extend(true, {}, defaults, opts); |
||||
|
||||
return this.each(() => { |
||||
const $tooltip = $(this); |
||||
let width, height; |
||||
|
||||
$tooltip.addClass('grafana-tooltip'); |
||||
|
||||
$('#tooltip').remove(); |
||||
$tooltip.appendTo(document.body); |
||||
|
||||
if (opts.compile) { |
||||
angular |
||||
.element(document) |
||||
.injector() |
||||
.invoke([ |
||||
'$compile', |
||||
'$rootScope', |
||||
($compile, $rootScope) => { |
||||
const tmpScope = $rootScope.$new(true); |
||||
extend(tmpScope, opts.scopeData); |
||||
|
||||
$compile($tooltip)(tmpScope); |
||||
tmpScope.$digest(); |
||||
tmpScope.$destroy(); |
||||
}, |
||||
]); |
||||
} |
||||
|
||||
width = $tooltip.outerWidth(true)!; |
||||
height = $tooltip.outerHeight(true)!; |
||||
|
||||
const left = x + opts.offset + width > $win.width()! ? x - opts.offset - width : x + opts.offset; |
||||
const top = y + opts.offset + height > $win.height()! ? y - opts.offset - height : y + opts.offset; |
||||
|
||||
$tooltip.css('left', left > 0 ? left : 0); |
||||
$tooltip.css('top', top > 0 ? top : 0); |
||||
}); |
||||
}; |
||||
})(); |
@ -1,23 +0,0 @@ |
||||
import { auto } from 'angular'; |
||||
|
||||
let injector: auto.IInjectorService | undefined; |
||||
|
||||
/** |
||||
* Future poc to lazy load angular app, not yet used |
||||
*/ |
||||
export async function getAngularInjector(): Promise<auto.IInjectorService> { |
||||
if (injector) { |
||||
return injector; |
||||
} |
||||
|
||||
const { AngularApp } = await import(/* webpackChunkName: "AngularApp" */ './index'); |
||||
if (injector) { |
||||
return injector; |
||||
} |
||||
|
||||
const app = new AngularApp(); |
||||
app.init(); |
||||
injector = app.bootstrap(); |
||||
|
||||
return injector; |
||||
} |
@ -1,92 +0,0 @@ |
||||
import { deprecationWarning } from '@grafana/data'; |
||||
import { |
||||
config, |
||||
setAngularLoader, |
||||
setLegacyAngularInjector, |
||||
getDataSourceSrv, |
||||
getBackendSrv, |
||||
getTemplateSrv, |
||||
} from '@grafana/runtime'; |
||||
import { contextSrv } from 'app/core/core'; |
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; |
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; |
||||
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; |
||||
import { getLinkSrv } from 'app/features/panel/panellinks/link_srv'; |
||||
|
||||
export async function loadAndInitAngularIfEnabled() { |
||||
if (config.angularSupportEnabled) { |
||||
const { AngularApp } = await import(/* webpackChunkName: "AngularApp" */ './index'); |
||||
const app = new AngularApp(); |
||||
app.init(); |
||||
app.bootstrap(); |
||||
} else { |
||||
// Register a dummy loader that does nothing
|
||||
setAngularLoader({ |
||||
load: (elem, scopeProps, template) => { |
||||
return { |
||||
destroy: () => {}, |
||||
digest: () => {}, |
||||
getScope: () => { |
||||
return {}; |
||||
}, |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
// Temporary path to allow access to services exposed directly by the angular injector
|
||||
setLegacyAngularInjector({ |
||||
get: (key: string) => { |
||||
switch (key) { |
||||
case 'backendSrv': { |
||||
deprecationWarning('getLegacyAngularInjector', 'backendSrv', 'use getBackendSrv() in @grafana/runtime'); |
||||
return getBackendSrv(); |
||||
} |
||||
|
||||
case 'contextSrv': { |
||||
deprecationWarning('getLegacyAngularInjector', 'contextSrv'); |
||||
return contextSrv; |
||||
} |
||||
|
||||
case 'dashboardSrv': { |
||||
// we do not yet have a public interface for this
|
||||
deprecationWarning('getLegacyAngularInjector', 'getDashboardSrv'); |
||||
return getDashboardSrv(); |
||||
} |
||||
|
||||
case 'datasourceSrv': { |
||||
deprecationWarning( |
||||
'getLegacyAngularInjector', |
||||
'datasourceSrv', |
||||
'use getDataSourceSrv() in @grafana/runtime' |
||||
); |
||||
return getDataSourceSrv(); |
||||
} |
||||
|
||||
case 'linkSrv': { |
||||
// we do not yet have a public interface for this
|
||||
deprecationWarning('getLegacyAngularInjector', 'linkSrv'); |
||||
return getLinkSrv(); |
||||
} |
||||
|
||||
case 'validationSrv': { |
||||
// we do not yet have a public interface for this
|
||||
deprecationWarning('getLegacyAngularInjector', 'validationSrv'); |
||||
return validationSrv; |
||||
} |
||||
|
||||
case 'timeSrv': { |
||||
// we do not yet have a public interface for this
|
||||
deprecationWarning('getLegacyAngularInjector', 'timeSrv'); |
||||
return getTimeSrv(); |
||||
} |
||||
|
||||
case 'templateSrv': { |
||||
deprecationWarning('getLegacyAngularInjector', 'templateSrv', 'use getTemplateSrv() in @grafana/runtime'); |
||||
return getTemplateSrv(); |
||||
} |
||||
} |
||||
throw 'Angular is disabled. Unable to expose: ' + key; |
||||
}, |
||||
} as angular.auto.IInjectorService); |
||||
} |
||||
} |
@ -1,266 +0,0 @@ |
||||
import $ from 'jquery'; |
||||
import { debounce, find, indexOf, map, escape, unescape } from 'lodash'; |
||||
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
export function metricSegment($compile: any, $sce: any, templateSrv: TemplateSrv) { |
||||
const inputTemplate = |
||||
'<input type="text" data-provide="typeahead" ' + |
||||
' class="gf-form-input input-medium"' + |
||||
' spellcheck="false" style="display:none"></input>'; |
||||
|
||||
const linkTemplate = |
||||
'<a class="gf-form-label" ng-class="segment.cssClass" ' + |
||||
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>'; |
||||
|
||||
const selectTemplate = |
||||
'<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' + |
||||
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>'; |
||||
|
||||
return { |
||||
scope: { |
||||
segment: '=', |
||||
getOptions: '&', |
||||
onChange: '&', |
||||
debounce: '@', |
||||
}, |
||||
link: ($scope: any, elem: any) => { |
||||
const $input = $(inputTemplate); |
||||
const segment = $scope.segment; |
||||
const $button = $(segment.selectMode ? selectTemplate : linkTemplate); |
||||
let options = null; |
||||
let cancelBlur: any = null; |
||||
let linkMode = true; |
||||
const debounceLookup = $scope.debounce; |
||||
|
||||
$input.appendTo(elem); |
||||
$button.appendTo(elem); |
||||
|
||||
$scope.updateVariableValue = (value: string) => { |
||||
if (value === '' || segment.value === value) { |
||||
return; |
||||
} |
||||
|
||||
$scope.$apply(() => { |
||||
const selected: any = find($scope.altSegments, { value: value }); |
||||
if (selected) { |
||||
segment.value = selected.value; |
||||
segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value)); |
||||
segment.fake = false; |
||||
segment.expandable = selected.expandable; |
||||
|
||||
if (selected.type) { |
||||
segment.type = selected.type; |
||||
} |
||||
} else if (segment.custom !== 'false') { |
||||
segment.value = value; |
||||
segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value)); |
||||
segment.expandable = true; |
||||
segment.fake = false; |
||||
} |
||||
|
||||
$scope.onChange(); |
||||
}); |
||||
}; |
||||
|
||||
$scope.switchToLink = (fromClick: boolean) => { |
||||
if (linkMode && !fromClick) { |
||||
return; |
||||
} |
||||
|
||||
clearTimeout(cancelBlur); |
||||
cancelBlur = null; |
||||
linkMode = true; |
||||
$input.hide(); |
||||
$button.show(); |
||||
$scope.updateVariableValue($input.val()); |
||||
}; |
||||
|
||||
$scope.inputBlur = () => { |
||||
// happens long before the click event on the typeahead options
|
||||
// need to have long delay because the blur
|
||||
cancelBlur = setTimeout($scope.switchToLink, 200); |
||||
}; |
||||
|
||||
$scope.source = (query: string, callback: any) => { |
||||
$scope.$apply(() => { |
||||
$scope.getOptions({ $query: query }).then((altSegments: any) => { |
||||
$scope.altSegments = altSegments; |
||||
options = map($scope.altSegments, (alt) => { |
||||
return escape(alt.value); |
||||
}); |
||||
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') { |
||||
if (!segment.fake && indexOf(options, segment.value) === -1) { |
||||
options.unshift(escape(segment.value)); |
||||
} |
||||
} |
||||
|
||||
callback(options); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
$scope.updater = (value: string) => { |
||||
value = unescape(value); |
||||
if (value === segment.value) { |
||||
clearTimeout(cancelBlur); |
||||
$input.focus(); |
||||
return value; |
||||
} |
||||
|
||||
$input.val(value); |
||||
$scope.switchToLink(true); |
||||
|
||||
return value; |
||||
}; |
||||
|
||||
$scope.matcher = function (item: string) { |
||||
if (linkMode) { |
||||
return false; |
||||
} |
||||
let str = this.query; |
||||
if (str[0] === '/') { |
||||
str = str.substring(1); |
||||
} |
||||
if (str[str.length - 1] === '/') { |
||||
str = str.substring(0, str.length - 1); |
||||
} |
||||
try { |
||||
return item.toLowerCase().match(str.toLowerCase()); |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
}; |
||||
|
||||
$input.attr('data-provide', 'typeahead'); |
||||
$input.typeahead({ |
||||
source: $scope.source, |
||||
minLength: 0, |
||||
items: 10000, |
||||
updater: $scope.updater, |
||||
matcher: $scope.matcher, |
||||
}); |
||||
|
||||
const typeahead = $input.data('typeahead'); |
||||
typeahead.lookup = function () { |
||||
this.query = this.$element.val() || ''; |
||||
const items = this.source(this.query, $.proxy(this.process, this)); |
||||
return items ? this.process(items) : items; |
||||
}; |
||||
|
||||
if (debounceLookup) { |
||||
typeahead.lookup = debounce(typeahead.lookup, 500, { leading: true }); |
||||
} |
||||
|
||||
$button.keydown((evt) => { |
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) { |
||||
$button.click(); |
||||
} |
||||
}); |
||||
|
||||
$button.click(() => { |
||||
options = null; |
||||
$input.css('width', Math.max($button.width()!, 80) + 16 + 'px'); |
||||
|
||||
$button.hide(); |
||||
$input.show(); |
||||
$input.focus(); |
||||
|
||||
linkMode = false; |
||||
|
||||
const typeahead = $input.data('typeahead'); |
||||
if (typeahead) { |
||||
$input.val(''); |
||||
typeahead.lookup(); |
||||
} |
||||
}); |
||||
|
||||
$input.blur($scope.inputBlur); |
||||
|
||||
$compile(elem.contents())($scope); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
export function metricSegmentModel(uiSegmentSrv: any) { |
||||
return { |
||||
template: |
||||
'<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>', |
||||
restrict: 'E', |
||||
scope: { |
||||
property: '=', |
||||
options: '=', |
||||
getOptions: '&', |
||||
onChange: '&', |
||||
}, |
||||
link: { |
||||
pre: function postLink($scope: any, elem: any, attrs: any) { |
||||
let cachedOptions: any; |
||||
|
||||
$scope.valueToSegment = (value: any) => { |
||||
const option: any = find($scope.options, { value: value }); |
||||
const segment = { |
||||
cssClass: attrs.cssClass, |
||||
custom: attrs.custom, |
||||
value: option ? option.text : value, |
||||
selectMode: attrs.selectMode, |
||||
}; |
||||
|
||||
return uiSegmentSrv.newSegment(segment); |
||||
}; |
||||
|
||||
$scope.getOptionsInternal = () => { |
||||
if ($scope.options) { |
||||
cachedOptions = $scope.options; |
||||
return Promise.resolve( |
||||
map($scope.options, (option) => { |
||||
return { value: option.text }; |
||||
}) |
||||
); |
||||
} else { |
||||
return $scope.getOptions().then((options: any) => { |
||||
cachedOptions = options; |
||||
return map(options, (option) => { |
||||
if (option.html) { |
||||
return option; |
||||
} |
||||
return { value: option.text }; |
||||
}); |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
$scope.onSegmentChange = () => { |
||||
if (cachedOptions) { |
||||
const option: any = find(cachedOptions, { text: $scope.segment.value }); |
||||
if (option && option.value !== $scope.property) { |
||||
$scope.property = option.value; |
||||
} else if (attrs.custom !== 'false') { |
||||
$scope.property = $scope.segment.value; |
||||
} |
||||
} else { |
||||
$scope.property = $scope.segment.value; |
||||
} |
||||
|
||||
// needs to call this after digest so
|
||||
// property is synced with outerscope
|
||||
$scope.$$postDigest(() => { |
||||
$scope.$apply(() => { |
||||
$scope.onChange(); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
$scope.segment = $scope.valueToSegment($scope.property); |
||||
}, |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('metricSegment', ['$compile', '$sce', 'templateSrv', metricSegment]); |
||||
coreModule.directive('metricSegmentModel', ['uiSegmentSrv', metricSegmentModel]); |
@ -1,189 +0,0 @@ |
||||
import angular from 'angular'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
coreModule.directive('tip', ['$compile', tip]); |
||||
|
||||
function tip($compile: any) { |
||||
return { |
||||
restrict: 'E', |
||||
link: (scope: any, elem: any, attrs: any) => { |
||||
let _t = |
||||
'<i class="grafana-tip fa fa-' + |
||||
(attrs.icon || 'question-circle') + |
||||
'" bs-tooltip="\'' + |
||||
// here we double-html-encode any special characters in the source string
|
||||
// this is needed so that the final html contains the encoded entities as they
|
||||
// will be decoded when _t is parsed by angular
|
||||
elem.text().replace(/[\'\"\\{}<>&]/g, (m: string) => '&#' + m.charCodeAt(0) + ';') + |
||||
'\'"></i>'; |
||||
elem.replaceWith($compile(angular.element(_t))(scope)); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('compile', ['$compile', compile]); |
||||
|
||||
function compile($compile: any) { |
||||
return { |
||||
restrict: 'A', |
||||
link: (scope: any, element: any, attrs: any) => { |
||||
scope.$watch( |
||||
(scope: any) => { |
||||
return scope.$eval(attrs.compile); |
||||
}, |
||||
(value: any) => { |
||||
element.html(value); |
||||
$compile(element.contents())(scope); |
||||
} |
||||
); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('watchChange', watchChange); |
||||
|
||||
function watchChange() { |
||||
return { |
||||
scope: { onchange: '&watchChange' }, |
||||
link: (scope: any, element: any) => { |
||||
element.on('input', () => { |
||||
scope.$apply(() => { |
||||
scope.onchange({ inputValue: element.val() }); |
||||
}); |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('editorOptBool', ['$compile', editorOptBool]); |
||||
|
||||
function editorOptBool($compile: any) { |
||||
return { |
||||
restrict: 'E', |
||||
link: (scope: any, elem: any, attrs: any) => { |
||||
const ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; |
||||
const tip = attrs.tip ? ' <tip>' + attrs.tip + '</tip>' : ''; |
||||
const showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : ''; |
||||
|
||||
const template = |
||||
'<div class="editor-option gf-form-checkbox text-center"' + |
||||
showIf + |
||||
'>' + |
||||
' <label for="' + |
||||
attrs.model + |
||||
'" class="small">' + |
||||
attrs.text + |
||||
tip + |
||||
'</label>' + |
||||
'<input class="cr1" id="' + |
||||
attrs.model + |
||||
'" type="checkbox" ' + |
||||
' ng-model="' + |
||||
attrs.model + |
||||
'"' + |
||||
ngchange + |
||||
' ng-checked="' + |
||||
attrs.model + |
||||
'"></input>' + |
||||
' <label for="' + |
||||
attrs.model + |
||||
'" class="cr1"></label>'; |
||||
elem.replaceWith($compile(angular.element(template))(scope)); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('editorCheckbox', ['$compile, $interpolate', editorCheckbox]); |
||||
|
||||
function editorCheckbox($compile: any, $interpolate: any) { |
||||
return { |
||||
restrict: 'E', |
||||
link: (scope: any, elem: any, attrs: any) => { |
||||
const text = $interpolate(attrs.text)(scope); |
||||
const model = $interpolate(attrs.model)(scope); |
||||
const ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; |
||||
const tip = attrs.tip ? ' <tip>' + attrs.tip + '</tip>' : ''; |
||||
const label = '<label for="' + scope.$id + model + '" class="checkbox-label">' + text + tip + '</label>'; |
||||
|
||||
let template = |
||||
'<input class="cr1" id="' + |
||||
scope.$id + |
||||
model + |
||||
'" type="checkbox" ' + |
||||
' ng-model="' + |
||||
model + |
||||
'"' + |
||||
ngchange + |
||||
' ng-checked="' + |
||||
model + |
||||
'"></input>' + |
||||
' <label for="' + |
||||
scope.$id + |
||||
model + |
||||
'" class="cr1"></label>'; |
||||
|
||||
template = template + label; |
||||
elem.addClass('gf-form-checkbox'); |
||||
elem.html($compile(angular.element(template))(scope)); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('gfDropdown', ['$parse', '$compile', '$timeout', gfDropdown]); |
||||
|
||||
function gfDropdown($parse: any, $compile: any, $timeout: any) { |
||||
function buildTemplate(items: any, placement?: any) { |
||||
const upclass = placement === 'top' ? 'dropup' : ''; |
||||
const ul = ['<ul class="dropdown-menu ' + upclass + '" role="menu" aria-labelledby="drop1">', '</ul>']; |
||||
|
||||
for (let index = 0; index < items.length; index++) { |
||||
const item = items[index]; |
||||
|
||||
if (item.divider) { |
||||
ul.splice(index + 1, 0, '<li class="divider"></li>'); |
||||
continue; |
||||
} |
||||
|
||||
let li = |
||||
'<li' + |
||||
(item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + |
||||
'>' + |
||||
'<a tabindex="-1" ng-href="' + |
||||
(item.href || '') + |
||||
'"' + |
||||
(item.click ? ' ng-click="' + item.click + '"' : '') + |
||||
(item.target ? ' target="' + item.target + '"' : '') + |
||||
(item.method ? ' data-method="' + item.method + '"' : '') + |
||||
'>' + |
||||
(item.text || '') + |
||||
'</a>'; |
||||
|
||||
if (item.submenu && item.submenu.length) { |
||||
li += buildTemplate(item.submenu).join('\n'); |
||||
} |
||||
|
||||
li += '</li>'; |
||||
ul.splice(index + 1, 0, li); |
||||
} |
||||
|
||||
return ul; |
||||
} |
||||
|
||||
return { |
||||
restrict: 'EA', |
||||
scope: true, |
||||
link: function postLink(scope: any, iElement: any, iAttrs: any) { |
||||
const getter = $parse(iAttrs.gfDropdown), |
||||
items = getter(scope); |
||||
$timeout(() => { |
||||
const placement = iElement.data('placement'); |
||||
const dropdown = angular.element(buildTemplate(items, placement).join('')); |
||||
dropdown.insertAfter(iElement); |
||||
$compile(iElement.next('ul.dropdown-menu'))(scope); |
||||
}); |
||||
|
||||
iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown'); |
||||
}, |
||||
}; |
||||
} |
@ -1,60 +0,0 @@ |
||||
import { rangeUtil } from '@grafana/data'; |
||||
|
||||
import coreModule from './core_module'; |
||||
|
||||
function ngModelOnBlur() { |
||||
return { |
||||
restrict: 'A', |
||||
priority: 1, |
||||
require: 'ngModel', |
||||
link: (scope: any, elm: any, attr: any, ngModelCtrl: any) => { |
||||
if (attr.type === 'radio' || attr.type === 'checkbox') { |
||||
return; |
||||
} |
||||
|
||||
elm.off('input keydown change'); |
||||
elm.bind('blur', () => { |
||||
scope.$apply(() => { |
||||
ngModelCtrl.$setViewValue(elm.val()); |
||||
}); |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
function emptyToNull() { |
||||
return { |
||||
restrict: 'A', |
||||
require: 'ngModel', |
||||
link: (scope: any, elm: any, attrs: any, ctrl: any) => { |
||||
ctrl.$parsers.push((viewValue: any) => { |
||||
if (viewValue === '') { |
||||
return null; |
||||
} |
||||
return viewValue; |
||||
}); |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
function validTimeSpan() { |
||||
return { |
||||
require: 'ngModel', |
||||
link: (scope: any, elm: any, attrs: any, ctrl: any) => { |
||||
ctrl.$validators.integer = (modelValue: any, viewValue: any) => { |
||||
if (ctrl.$isEmpty(modelValue)) { |
||||
return true; |
||||
} |
||||
if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) { |
||||
return true; // allow template variable
|
||||
} |
||||
const info = rangeUtil.describeTextRange(viewValue); |
||||
return info.invalid !== true; |
||||
}; |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
coreModule.directive('ngModelOnblur', ngModelOnBlur); |
||||
coreModule.directive('emptyToNull', emptyToNull); |
||||
coreModule.directive('validTimeSpan', validTimeSpan); |
@ -1,127 +0,0 @@ |
||||
import { ComponentType, useEffect, useRef } from 'react'; |
||||
import { Observable, ReplaySubject } from 'rxjs'; |
||||
|
||||
import { EventBusSrv, PanelData, PanelPlugin, PanelProps, FieldConfigSource } from '@grafana/data'; |
||||
import { AngularComponent, getAngularLoader, RefreshEvent } from '@grafana/runtime'; |
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; |
||||
import { DashboardModelCompatibilityWrapper } from 'app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper'; |
||||
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner'; |
||||
import { RenderEvent } from 'app/types/events'; |
||||
|
||||
interface AngularScopeProps { |
||||
panel: PanelModelCompatibilityWrapper; |
||||
dashboard: DashboardModelCompatibilityWrapper; |
||||
queryRunner: FakeQueryRunner; |
||||
size: { |
||||
height: number; |
||||
width: number; |
||||
}; |
||||
} |
||||
|
||||
export function getAngularPanelReactWrapper(plugin: PanelPlugin): ComponentType<PanelProps> { |
||||
return function AngularWrapper(props: PanelProps) { |
||||
const divRef = useRef<HTMLDivElement>(null); |
||||
const angularState = useRef<AngularScopeProps | undefined>(); |
||||
const angularComponent = useRef<AngularComponent | undefined>(); |
||||
|
||||
useEffect(() => { |
||||
if (!divRef.current) { |
||||
return; |
||||
} |
||||
|
||||
const loader = getAngularLoader(); |
||||
const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>'; |
||||
const queryRunner = new FakeQueryRunner(); |
||||
const fakePanel = new PanelModelCompatibilityWrapper(plugin, props, queryRunner); |
||||
|
||||
angularState.current = { |
||||
// @ts-ignore
|
||||
panel: fakePanel, |
||||
// @ts-ignore
|
||||
dashboard: getDashboardSrv().getCurrent(), |
||||
size: { width: props.width, height: props.height }, |
||||
queryRunner: queryRunner, |
||||
}; |
||||
|
||||
angularComponent.current = loader.load(divRef.current, angularState.current, template); |
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); |
||||
|
||||
// Re-render angular panel when dimensions change
|
||||
useEffect(() => { |
||||
if (!angularComponent.current) { |
||||
return; |
||||
} |
||||
|
||||
angularState.current!.size.height = props.height; |
||||
angularState.current!.size.width = props.width; |
||||
angularState.current!.panel.events.publish(new RenderEvent()); |
||||
}, [props.width, props.height]); |
||||
|
||||
// Pass new data to angular panel
|
||||
useEffect(() => { |
||||
if (!angularState.current?.panel) { |
||||
return; |
||||
} |
||||
|
||||
angularState.current.queryRunner.forwardNewData(props.data); |
||||
}, [props.data]); |
||||
|
||||
return <div ref={divRef} className="panel-height-helper" />; |
||||
}; |
||||
} |
||||
|
||||
class PanelModelCompatibilityWrapper { |
||||
id: number; |
||||
type: string; |
||||
title: string; |
||||
plugin: PanelPlugin; |
||||
events: EventBusSrv; |
||||
queryRunner: FakeQueryRunner; |
||||
fieldConfig: FieldConfigSource; |
||||
options: Record<string, unknown>; |
||||
|
||||
constructor(plugin: PanelPlugin, props: PanelProps, queryRunner: FakeQueryRunner) { |
||||
// Assign legacy "root" level options
|
||||
if (props.options.angularOptions) { |
||||
Object.assign(this, props.options.angularOptions); |
||||
} |
||||
|
||||
this.id = props.id; |
||||
this.type = plugin.meta.id; |
||||
this.title = props.title; |
||||
this.fieldConfig = props.fieldConfig; |
||||
this.options = props.options; |
||||
|
||||
this.plugin = plugin; |
||||
this.events = new EventBusSrv(); |
||||
this.queryRunner = queryRunner; |
||||
} |
||||
|
||||
refresh() { |
||||
this.events.publish(new RefreshEvent()); |
||||
} |
||||
|
||||
render() { |
||||
this.events.publish(new RenderEvent()); |
||||
} |
||||
|
||||
getQueryRunner() { |
||||
return this.queryRunner; |
||||
} |
||||
} |
||||
|
||||
class FakeQueryRunner { |
||||
private subject = new ReplaySubject<PanelData>(1); |
||||
|
||||
getData(options: GetDataOptions): Observable<PanelData> { |
||||
return this.subject; |
||||
} |
||||
|
||||
forwardNewData(data: PanelData) { |
||||
this.subject.next(data); |
||||
} |
||||
|
||||
run() {} |
||||
} |
@ -1,4 +0,0 @@ |
||||
import './panel_directive'; |
||||
import './query_ctrl'; |
||||
import './panel_editor_tab'; |
||||
import './query_editor_row'; |
@ -1,244 +0,0 @@ |
||||
import { isArray } from 'lodash'; |
||||
import { Unsubscribable } from 'rxjs'; |
||||
|
||||
import { |
||||
DataFrame, |
||||
DataQueryResponse, |
||||
DataSourceApi, |
||||
LegacyResponseData, |
||||
LoadingState, |
||||
PanelData, |
||||
PanelEvents, |
||||
TimeRange, |
||||
toDataFrameDTO, |
||||
toLegacyResponseData, |
||||
} from '@grafana/data'; |
||||
import { PanelCtrl } from 'app/angular/panel/panel_ctrl'; |
||||
import { ContextSrv } from 'app/core/services/context_srv'; |
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; |
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; |
||||
|
||||
import { PanelQueryRunner } from '../../features/query/state/PanelQueryRunner'; |
||||
|
||||
class MetricsPanelCtrl extends PanelCtrl { |
||||
declare datasource: DataSourceApi; |
||||
declare range: TimeRange; |
||||
|
||||
contextSrv: ContextSrv; |
||||
datasourceSrv: any; |
||||
timeSrv: any; |
||||
templateSrv: any; |
||||
interval: any; |
||||
intervalMs: any; |
||||
resolution: any; |
||||
timeInfo?: string; |
||||
skipDataOnInit = false; |
||||
dataList: LegacyResponseData[] = []; |
||||
querySubscription?: Unsubscribable | null; |
||||
useDataFrames = false; |
||||
panelData?: PanelData; |
||||
|
||||
constructor($scope: any, $injector: any) { |
||||
super($scope, $injector); |
||||
|
||||
this.contextSrv = $injector.get('contextSrv'); |
||||
this.datasourceSrv = $injector.get('datasourceSrv'); |
||||
this.timeSrv = $injector.get('timeSrv'); |
||||
this.templateSrv = $injector.get('templateSrv'); |
||||
this.panel.datasource = this.panel.datasource || null; |
||||
|
||||
this.events.on(PanelEvents.refresh, this.onMetricsPanelRefresh.bind(this)); |
||||
this.events.on(PanelEvents.panelTeardown, this.onPanelTearDown.bind(this)); |
||||
this.events.on(PanelEvents.componentDidMount, this.onMetricsPanelMounted.bind(this)); |
||||
} |
||||
|
||||
private onMetricsPanelMounted() { |
||||
const queryRunner = this.panel.getQueryRunner() as PanelQueryRunner; |
||||
this.querySubscription = queryRunner |
||||
.getData({ withTransforms: true, withFieldConfig: true }) |
||||
.subscribe(this.panelDataObserver); |
||||
} |
||||
|
||||
private onPanelTearDown() { |
||||
if (this.querySubscription) { |
||||
this.querySubscription.unsubscribe(); |
||||
this.querySubscription = null; |
||||
} |
||||
} |
||||
|
||||
private onMetricsPanelRefresh() { |
||||
// ignore fetching data if another panel is in fullscreen
|
||||
if (this.otherPanelInFullscreenMode()) { |
||||
return; |
||||
} |
||||
|
||||
// if we have snapshot data use that
|
||||
if (this.panel.snapshotData) { |
||||
this.updateTimeRange(); |
||||
let data = this.panel.snapshotData; |
||||
// backward compatibility
|
||||
if (!isArray(data)) { |
||||
data = data.data; |
||||
} |
||||
|
||||
this.panelData = { |
||||
state: LoadingState.Done, |
||||
series: data, |
||||
timeRange: this.range, |
||||
}; |
||||
|
||||
// Defer panel rendering till the next digest cycle.
|
||||
// For some reason snapshot panels don't init at this time, so this helps to avoid rendering issues.
|
||||
return this.$timeout(() => { |
||||
this.events.emit(PanelEvents.dataSnapshotLoad, data); |
||||
}); |
||||
} |
||||
|
||||
// clear loading/error state
|
||||
delete this.error; |
||||
this.loading = true; |
||||
|
||||
// load datasource service
|
||||
return this.datasourceSrv |
||||
.get(this.panel.datasource, this.panel.scopedVars) |
||||
.then(this.issueQueries.bind(this)) |
||||
.catch((err: any) => { |
||||
this.processDataError(err); |
||||
}); |
||||
} |
||||
|
||||
processDataError(err: any) { |
||||
// if canceled keep loading set to true
|
||||
if (err.cancelled) { |
||||
console.log('Panel request cancelled', err); |
||||
return; |
||||
} |
||||
|
||||
this.error = err.message || 'Request Error'; |
||||
|
||||
if (err.data) { |
||||
if (err.data.message) { |
||||
this.error = err.data.message; |
||||
} else if (err.data.error) { |
||||
this.error = err.data.error; |
||||
} |
||||
} |
||||
|
||||
this.angularDirtyCheck(); |
||||
} |
||||
|
||||
angularDirtyCheck() { |
||||
if (!this.$scope.$root.$$phase) { |
||||
this.$scope.$digest(); |
||||
} |
||||
} |
||||
|
||||
// Updates the response with information from the stream
|
||||
panelDataObserver = { |
||||
next: (data: PanelData) => { |
||||
this.panelData = data; |
||||
|
||||
if (data.state === LoadingState.Error) { |
||||
this.loading = false; |
||||
this.processDataError(data.error); |
||||
} |
||||
|
||||
// Ignore data in loading state
|
||||
if (data.state === LoadingState.Loading) { |
||||
this.loading = true; |
||||
this.angularDirtyCheck(); |
||||
return; |
||||
} |
||||
|
||||
if (data.request) { |
||||
const { timeInfo } = data.request; |
||||
if (timeInfo) { |
||||
this.timeInfo = timeInfo; |
||||
} |
||||
} |
||||
|
||||
if (data.timeRange) { |
||||
this.range = data.timeRange; |
||||
} |
||||
|
||||
if (this.useDataFrames) { |
||||
this.handleDataFrames(data.series); |
||||
} else { |
||||
// Make the results look as if they came directly from a <6.2 datasource request
|
||||
const legacy = data.series.map((v) => toLegacyResponseData(v)); |
||||
this.handleQueryResult({ data: legacy }); |
||||
} |
||||
|
||||
this.angularDirtyCheck(); |
||||
}, |
||||
}; |
||||
|
||||
updateTimeRange(datasource?: DataSourceApi) { |
||||
this.datasource = datasource || this.datasource; |
||||
this.range = this.timeSrv.timeRange(); |
||||
|
||||
const newTimeData = applyPanelTimeOverrides(this.panel, this.range); |
||||
this.timeInfo = newTimeData.timeInfo; |
||||
this.range = newTimeData.timeRange; |
||||
} |
||||
|
||||
issueQueries(datasource: DataSourceApi) { |
||||
this.updateTimeRange(datasource); |
||||
|
||||
this.datasource = datasource; |
||||
|
||||
const panel = this.panel as PanelModel; |
||||
const queryRunner = panel.getQueryRunner(); |
||||
|
||||
return queryRunner.run({ |
||||
datasource: panel.datasource, |
||||
queries: panel.targets, |
||||
panelId: panel.id, |
||||
dashboardUID: this.dashboard.uid, |
||||
timezone: this.dashboard.getTimezone(), |
||||
timeInfo: this.timeInfo, |
||||
timeRange: this.range, |
||||
maxDataPoints: panel.maxDataPoints || this.width, |
||||
minInterval: panel.interval, |
||||
scopedVars: panel.scopedVars, |
||||
cacheTimeout: panel.cacheTimeout, |
||||
queryCachingTTL: panel.queryCachingTTL, |
||||
transformations: panel.transformations, |
||||
}); |
||||
} |
||||
|
||||
handleDataFrames(data: DataFrame[]) { |
||||
this.loading = false; |
||||
|
||||
if (this.dashboard && this.dashboard.snapshot) { |
||||
this.panel.snapshotData = data.map((frame) => toDataFrameDTO(frame)); |
||||
} |
||||
|
||||
try { |
||||
this.events.emit(PanelEvents.dataFramesReceived, data); |
||||
} catch (err) { |
||||
this.processDataError(err); |
||||
} |
||||
} |
||||
|
||||
handleQueryResult(result: DataQueryResponse) { |
||||
this.loading = false; |
||||
|
||||
if (this.dashboard.snapshot) { |
||||
this.panel.snapshotData = result.data; |
||||
} |
||||
|
||||
if (!result || !result.data) { |
||||
console.log('Data source query result invalid, missing data field:', result); |
||||
result = { data: [] }; |
||||
} |
||||
|
||||
try { |
||||
this.events.emit(PanelEvents.dataReceived, result.data); |
||||
} catch (err) { |
||||
this.processDataError(err); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export { MetricsPanelCtrl }; |
@ -1,119 +0,0 @@ |
||||
import { auto } from 'angular'; |
||||
import { isString } from 'lodash'; |
||||
|
||||
import { |
||||
AppEvent, |
||||
PanelEvents, |
||||
PanelPluginMeta, |
||||
AngularPanelMenuItem, |
||||
EventBusExtended, |
||||
EventBusSrv, |
||||
} from '@grafana/data'; |
||||
import { AngularLocationWrapper } from 'app/angular/AngularLocationWrapper'; |
||||
import config from 'app/core/config'; |
||||
import { profiler } from 'app/core/core'; |
||||
|
||||
import { DashboardModel } from '../../features/dashboard/state/DashboardModel'; |
||||
|
||||
export class PanelCtrl { |
||||
panel: any; |
||||
error: any; |
||||
declare dashboard: DashboardModel; |
||||
pluginName = ''; |
||||
pluginId = ''; |
||||
editorTabs: any; |
||||
$scope: any; |
||||
$injector: auto.IInjectorService; |
||||
$timeout: any; |
||||
editModeInitiated = false; |
||||
declare height: number; |
||||
declare width: number; |
||||
containerHeight: any; |
||||
events: EventBusExtended; |
||||
loading = false; |
||||
timing: any; |
||||
$location: AngularLocationWrapper; |
||||
|
||||
constructor($scope: any, $injector: auto.IInjectorService) { |
||||
this.panel = this.panel ?? $scope.$parent.panel; |
||||
this.dashboard = this.dashboard ?? $scope.$parent.dashboard; |
||||
this.$injector = $injector; |
||||
this.$scope = $scope; |
||||
this.$timeout = $injector.get('$timeout'); |
||||
this.editorTabs = []; |
||||
this.$location = new AngularLocationWrapper(); |
||||
this.events = new EventBusSrv(); |
||||
this.timing = {}; // not used but here to not break plugins
|
||||
|
||||
const plugin = config.panels[this.panel.type]; |
||||
if (plugin) { |
||||
this.pluginId = plugin.id; |
||||
this.pluginName = plugin.name; |
||||
} |
||||
|
||||
$scope.$on(PanelEvents.componentDidMount.name, () => this.panelDidMount()); |
||||
} |
||||
|
||||
panelDidMount() { |
||||
this.events.emit(PanelEvents.componentDidMount); |
||||
this.events.emit(PanelEvents.initialized); |
||||
this.dashboard.panelInitialized(this.panel); |
||||
} |
||||
|
||||
renderingCompleted() { |
||||
profiler.renderingCompleted(); |
||||
} |
||||
|
||||
refresh() { |
||||
this.panel.refresh(); |
||||
} |
||||
|
||||
publishAppEvent<T>(event: AppEvent<T>, payload?: T) { |
||||
this.$scope.$root.appEvent(event, payload); |
||||
} |
||||
|
||||
initEditMode() { |
||||
if (!this.editModeInitiated) { |
||||
this.editModeInitiated = true; |
||||
this.events.emit(PanelEvents.editModeInitialized); |
||||
} |
||||
} |
||||
|
||||
addEditorTab(title: string, directiveFn: any, index?: number, icon?: any) { |
||||
const editorTab = { title, directiveFn, icon }; |
||||
|
||||
if (isString(directiveFn)) { |
||||
editorTab.directiveFn = () => { |
||||
return { templateUrl: directiveFn }; |
||||
}; |
||||
} |
||||
|
||||
if (index) { |
||||
this.editorTabs.splice(index, 0, editorTab); |
||||
} else { |
||||
this.editorTabs.push(editorTab); |
||||
} |
||||
} |
||||
|
||||
getExtendedMenu() { |
||||
const menu: AngularPanelMenuItem[] = []; |
||||
this.events.emit(PanelEvents.initPanelActions, menu); |
||||
return menu; |
||||
} |
||||
|
||||
// Override in sub-class to add items before extended menu
|
||||
async getAdditionalMenuItems(): Promise<any[]> { |
||||
return []; |
||||
} |
||||
|
||||
otherPanelInFullscreenMode() { |
||||
return this.dashboard.otherPanelInFullscreen(this.panel); |
||||
} |
||||
|
||||
render(payload?: any) { |
||||
this.events.emit(PanelEvents.render, payload); |
||||
} |
||||
|
||||
// overriden from react
|
||||
onPluginTypeChange = (plugin: PanelPluginMeta) => {}; |
||||
} |
@ -1,127 +0,0 @@ |
||||
// @ts-ignore
|
||||
import baron from 'baron'; |
||||
import { Subscription } from 'rxjs'; |
||||
|
||||
import { PanelEvents } from '@grafana/data'; |
||||
import { RefreshEvent } from '@grafana/runtime'; |
||||
import { coreModule } from 'app/angular/core_module'; |
||||
import { PanelDirectiveReadyEvent, RenderEvent } from 'app/types/events'; |
||||
|
||||
import { PanelModel } from '../../features/dashboard/state/PanelModel'; |
||||
|
||||
import { PanelCtrl } from './panel_ctrl'; |
||||
|
||||
const panelTemplate = ` |
||||
<ng-transclude class="panel-height-helper"></ng-transclude> |
||||
`;
|
||||
|
||||
coreModule.directive('grafanaPanel', [ |
||||
'$timeout', |
||||
($timeout) => { |
||||
return { |
||||
restrict: 'E', |
||||
template: panelTemplate, |
||||
transclude: true, |
||||
scope: { ctrl: '=' }, |
||||
link: (scope: any, elem) => { |
||||
const ctrl: PanelCtrl = scope.ctrl; |
||||
const panel: PanelModel = scope.ctrl.panel; |
||||
const subs = new Subscription(); |
||||
|
||||
let panelScrollbar: any; |
||||
|
||||
function resizeScrollableContent() { |
||||
if (panelScrollbar) { |
||||
panelScrollbar.update(); |
||||
} |
||||
} |
||||
|
||||
ctrl.events.on(PanelEvents.componentDidMount, () => { |
||||
if ((ctrl as any).__proto__.constructor.scrollable) { |
||||
const scrollRootClass = 'baron baron__root baron__clipper panel-content--scrollable'; |
||||
const scrollerClass = 'baron__scroller'; |
||||
const scrollBarHTML = ` |
||||
<div class="baron__track"> |
||||
<div class="baron__bar"></div> |
||||
</div> |
||||
`;
|
||||
|
||||
const scrollRoot = elem; |
||||
const scroller = elem.find(':first').find(':first'); |
||||
|
||||
scrollRoot.addClass(scrollRootClass); |
||||
$(scrollBarHTML).appendTo(scrollRoot); |
||||
scroller.addClass(scrollerClass); |
||||
|
||||
panelScrollbar = baron({ |
||||
root: scrollRoot[0], |
||||
scroller: scroller[0], |
||||
bar: '.baron__bar', |
||||
barOnCls: '_scrollbar', |
||||
scrollingCls: '_scrolling', |
||||
}); |
||||
|
||||
panelScrollbar.scroll(); |
||||
} |
||||
}); |
||||
|
||||
function updateDimensionsFromParentScope() { |
||||
ctrl.height = scope.$parent.$parent.size.height; |
||||
ctrl.width = scope.$parent.$parent.size.width; |
||||
} |
||||
|
||||
updateDimensionsFromParentScope(); |
||||
|
||||
// Pass PanelModel events down to angular controller event emitter
|
||||
subs.add( |
||||
panel.events.subscribe(RefreshEvent, () => { |
||||
updateDimensionsFromParentScope(); |
||||
ctrl.events.emit('refresh'); |
||||
}) |
||||
); |
||||
|
||||
subs.add( |
||||
panel.events.subscribe(RenderEvent, (event) => { |
||||
// this event originated from angular so no need to bubble it back
|
||||
if (event.payload?.fromAngular) { |
||||
return; |
||||
} |
||||
|
||||
updateDimensionsFromParentScope(); |
||||
|
||||
$timeout(() => { |
||||
resizeScrollableContent(); |
||||
ctrl.events.emit('render'); |
||||
}); |
||||
}) |
||||
); |
||||
|
||||
subs.add( |
||||
ctrl.events.subscribe(RenderEvent, (event) => { |
||||
// this event originated from angular so bubble it to react so the PanelChromeAngular can update the panel header alert state
|
||||
if (event.payload) { |
||||
event.payload.fromAngular = true; |
||||
panel.events.publish(event); |
||||
} |
||||
}) |
||||
); |
||||
|
||||
scope.$on('$destroy', () => { |
||||
elem.off(); |
||||
|
||||
// Remove PanelModel.event subs
|
||||
subs.unsubscribe(); |
||||
// Remove Angular controller event subs
|
||||
ctrl.events.emit(PanelEvents.panelTeardown); |
||||
ctrl.events.removeAllListeners(); |
||||
|
||||
if (panelScrollbar) { |
||||
panelScrollbar.dispose(); |
||||
} |
||||
}); |
||||
|
||||
panel.events.publish(PanelDirectiveReadyEvent); |
||||
}, |
||||
}; |
||||
}, |
||||
]); |
@ -1,41 +0,0 @@ |
||||
import angular from 'angular'; |
||||
|
||||
const directiveModule = angular.module('grafana.directives'); |
||||
const directiveCache: any = {}; |
||||
|
||||
directiveModule.directive('panelEditorTab', ['dynamicDirectiveSrv', panelEditorTab]); |
||||
|
||||
function panelEditorTab(dynamicDirectiveSrv: any) { |
||||
return dynamicDirectiveSrv.create({ |
||||
scope: { |
||||
ctrl: '=', |
||||
editorTab: '=', |
||||
}, |
||||
directive: (scope: any) => { |
||||
const pluginId = scope.ctrl.pluginId; |
||||
const tabName = scope.editorTab.title |
||||
.toLowerCase() |
||||
.replace(' ', '-') |
||||
.replace('&', '') |
||||
.replace(' ', '') |
||||
.replace(' ', '-'); |
||||
|
||||
if (directiveCache[pluginId]) { |
||||
if (directiveCache[pluginId][tabName]) { |
||||
return directiveCache[pluginId][tabName]; |
||||
} |
||||
} else { |
||||
directiveCache[pluginId] = []; |
||||
} |
||||
|
||||
const result = { |
||||
fn: () => scope.editorTab.directiveFn(), |
||||
name: `panel-editor-tab-${pluginId}${tabName}`, |
||||
}; |
||||
|
||||
directiveCache[pluginId][tabName] = result; |
||||
|
||||
return result; |
||||
}, |
||||
}); |
||||
} |
@ -1,2 +0,0 @@ |
||||
<div ng-transclude class="gf-form-query-content"></div> |
||||
|
@ -1,27 +0,0 @@ |
||||
import { auto } from 'angular'; |
||||
import { indexOf } from 'lodash'; |
||||
|
||||
export class QueryCtrl<T = any> { |
||||
target!: T; |
||||
datasource!: any; |
||||
panelCtrl!: any; |
||||
panel: any; |
||||
hasRawMode!: boolean; |
||||
error?: string | null; |
||||
isLastQuery: boolean; |
||||
|
||||
constructor( |
||||
public $scope: any, |
||||
public $injector: auto.IInjectorService |
||||
) { |
||||
this.panelCtrl = this.panelCtrl ?? $scope.ctrl.panelCtrl; |
||||
this.target = this.target ?? $scope.ctrl.target; |
||||
this.datasource = this.datasource ?? $scope.ctrl.datasource; |
||||
this.panel = this.panelCtrl?.panel ?? $scope.ctrl.panelCtrl.panel; |
||||
this.isLastQuery = indexOf(this.panel.targets, this.target) === this.panel.targets.length - 1; |
||||
} |
||||
|
||||
refresh() { |
||||
this.panelCtrl.refresh(); |
||||
} |
||||
} |
@ -1,43 +0,0 @@ |
||||
import { coreModule } from 'app/angular/core_module'; |
||||
|
||||
export class QueryRowCtrl { |
||||
target: any; |
||||
queryCtrl: any; |
||||
panelCtrl: any; |
||||
panel: any; |
||||
hasTextEditMode = false; |
||||
|
||||
$onInit() { |
||||
this.panelCtrl = this.queryCtrl.panelCtrl; |
||||
this.target = this.queryCtrl.target; |
||||
this.panel = this.panelCtrl.panel; |
||||
|
||||
if (this.hasTextEditMode && this.queryCtrl.toggleEditorMode) { |
||||
// expose this function to react parent component
|
||||
this.panelCtrl.toggleEditorMode = this.queryCtrl.toggleEditorMode.bind(this.queryCtrl); |
||||
} |
||||
|
||||
if (this.queryCtrl.getCollapsedText) { |
||||
// expose this function to react parent component
|
||||
this.panelCtrl.getCollapsedText = this.queryCtrl.getCollapsedText.bind(this.queryCtrl); |
||||
} |
||||
} |
||||
} |
||||
|
||||
coreModule.directive('queryEditorRow', queryEditorRowDirective); |
||||
|
||||
function queryEditorRowDirective() { |
||||
return { |
||||
restrict: 'E', |
||||
controller: QueryRowCtrl, |
||||
bindToController: true, |
||||
controllerAs: 'ctrl', |
||||
templateUrl: 'public/app/angular/panel/partials/query_editor_row.html', |
||||
transclude: true, |
||||
scope: { |
||||
queryCtrl: '=', |
||||
canCollapse: '=', |
||||
hasTextEditMode: '=', |
||||
}, |
||||
}; |
||||
} |
@ -1,57 +0,0 @@ |
||||
jest.mock('app/core/core', () => ({})); |
||||
jest.mock('app/core/config', () => { |
||||
return { |
||||
...jest.requireActual('app/core/config'), |
||||
panels: { |
||||
test: { |
||||
id: 'test', |
||||
name: 'test', |
||||
}, |
||||
}, |
||||
}; |
||||
}); |
||||
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; |
||||
|
||||
import { MetricsPanelCtrl } from '../metrics_panel_ctrl'; |
||||
|
||||
describe('MetricsPanelCtrl', () => { |
||||
describe('can setup', () => { |
||||
it('should return controller', async () => { |
||||
const ctrl = setupController({ hasAccessToExplore: true }); |
||||
expect((await ctrl.getAdditionalMenuItems()).length).toBe(0); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
function setupController({ hasAccessToExplore } = { hasAccessToExplore: false }) { |
||||
const injectorStub = { |
||||
get: (type: any) => { |
||||
switch (type) { |
||||
case 'contextSrv': { |
||||
return { hasAccessToExplore: () => hasAccessToExplore }; |
||||
} |
||||
case 'timeSrv': { |
||||
return { timeRangeForUrl: () => {} }; |
||||
} |
||||
default: { |
||||
return jest.fn(); |
||||
} |
||||
} |
||||
}, |
||||
}; |
||||
|
||||
const scope: any = { |
||||
panel: { events: [] }, |
||||
appEvent: jest.fn(), |
||||
onAppEvent: jest.fn(), |
||||
$on: jest.fn(), |
||||
colors: [], |
||||
$parent: { |
||||
panel: new PanelModel({ type: 'test' }), |
||||
dashboard: {}, |
||||
}, |
||||
}; |
||||
|
||||
return new MetricsPanelCtrl(scope, injectorStub); |
||||
} |
@ -1,4 +0,0 @@ |
||||
let templates = (require as any).context('../', true, /\.html$/); |
||||
templates.keys().forEach((key: string) => { |
||||
templates(key); |
||||
}); |
@ -1,8 +0,0 @@ |
||||
<datasource-http-settings-next |
||||
on-change="onChange" |
||||
dataSourceConfig="current" |
||||
showAccessOptions="showAccessOption" |
||||
defaultUrl="suggestUrl" |
||||
showForwardOAuthIdentityOption="showForwardOAuthIdentityOption" |
||||
secureSocksDSProxyEnabled="secureSocksDSProxyEnabled" |
||||
/> |
@ -1,81 +0,0 @@ |
||||
<div class="gf-form-group"> |
||||
<div class="gf-form"> |
||||
<h6>TLS/SSL Auth Details</h6> |
||||
<info-popover mode="header">TLS/SSL certificates are encrypted and stored in the Grafana database.</info-popover> |
||||
</div> |
||||
<div ng-if="current.jsonData.tlsAuthWithCACert"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">CA Cert</label></div> |
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsCACert"> |
||||
<textarea |
||||
rows="7" |
||||
class="gf-form-input gf-form-textarea" |
||||
ng-model="current.secureJsonData.tlsCACert" |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
></textarea> |
||||
</div> |
||||
|
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsCACert"> |
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" /> |
||||
<button |
||||
type="reset" |
||||
aria-label="Reset CA Cert" |
||||
class="btn btn-secondary gf-form-btn" |
||||
ng-click="current.secureJsonFields.tlsCACert = false" |
||||
> |
||||
reset |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div ng-if="current.jsonData.tlsAuth"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">Client Cert</label></div> |
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientCert"> |
||||
<textarea |
||||
rows="7" |
||||
class="gf-form-input gf-form-textarea" |
||||
ng-model="current.secureJsonData.tlsClientCert" |
||||
placeholder="Begins with -----BEGIN CERTIFICATE-----" |
||||
required |
||||
></textarea> |
||||
</div> |
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientCert"> |
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" /> |
||||
<button |
||||
class="btn btn-secondary gf-form-btn" |
||||
aria-label="Reset Client Cert" |
||||
type="reset" |
||||
ng-click="current.secureJsonFields.tlsClientCert = false" |
||||
> |
||||
reset |
||||
</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form gf-form--v-stretch"><label class="gf-form-label width-7">Client Key</label></div> |
||||
<div class="gf-form gf-form--grow" ng-if="!current.secureJsonFields.tlsClientKey"> |
||||
<textarea |
||||
rows="7" |
||||
class="gf-form-input gf-form-textarea" |
||||
ng-model="current.secureJsonData.tlsClientKey" |
||||
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" |
||||
required |
||||
></textarea> |
||||
</div> |
||||
<div class="gf-form" ng-if="current.secureJsonFields.tlsClientKey"> |
||||
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured" /> |
||||
<button |
||||
class="btn btn-secondary gf-form-btn" |
||||
type="reset" |
||||
aria-label="Reset Client Key" |
||||
ng-click="current.secureJsonFields.tlsClientKey = false" |
||||
> |
||||
reset |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -1,28 +0,0 @@ |
||||
import { IScope } from 'angular'; |
||||
|
||||
import { promiseToDigest } from './promiseToDigest'; |
||||
|
||||
describe('promiseToDigest', () => { |
||||
describe('when called with a promise that resolves', () => { |
||||
it('then evalAsync should be called on $scope', async () => { |
||||
const $scope = { $evalAsync: jest.fn() } as jest.MockedObject<IScope>; |
||||
|
||||
await promiseToDigest($scope)(Promise.resolve(123)); |
||||
|
||||
expect($scope.$evalAsync).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with a promise that rejects', () => { |
||||
it('then evalAsync should be called on $scope', async () => { |
||||
const $scope = { $evalAsync: jest.fn() } as jest.MockedObject<IScope>; |
||||
|
||||
try { |
||||
await promiseToDigest($scope)(Promise.reject(123)); |
||||
} catch (error) { |
||||
expect(error).toEqual(123); |
||||
expect($scope.$evalAsync).toHaveBeenCalledTimes(1); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue