mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Angular: Isolate angular more (#41440)
* Getting close * Restore angular app boot at startup * Moving angular annotations dependencies to app/angular or old graph * Remove redundant setLinkSrv call * Fixing graph test * Minor refactor based on review feedback * Create in get function
This commit is contained in:
parent
e7fd41d779
commit
c96c92d712
@ -15,6 +15,7 @@ import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
||||
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||
import { SearchWrapper } from 'app/features/search';
|
||||
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||
import { AngularRoot } from './angular/AngularRoot';
|
||||
|
||||
interface AppWrapperProps {
|
||||
app: GrafanaApp;
|
||||
@ -57,6 +58,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
bootstrapNgApp() {
|
||||
const injector = this.props.app.angularApp.bootstrap();
|
||||
this.setState({ ngInjector: injector });
|
||||
$('.preloader').remove();
|
||||
}
|
||||
|
||||
renderRoute = (route: RouteDescriptor) => {
|
||||
@ -89,8 +91,6 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
render() {
|
||||
navigationLogger('AppWrapper', false, 'rendering');
|
||||
|
||||
// @ts-ignore
|
||||
const appSeed = `<grafana-app ng-cloak></app-notifications-list></grafana-app>`;
|
||||
const newNavigationEnabled = config.featureToggles.newNavigation;
|
||||
|
||||
return (
|
||||
@ -108,17 +108,10 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
<Banner key={index.toString()} />
|
||||
))}
|
||||
|
||||
<div
|
||||
id="ngRoot"
|
||||
ref={this.container}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: appSeed,
|
||||
}}
|
||||
/>
|
||||
|
||||
<AngularRoot ref={this.container} />
|
||||
<AppNotificationList />
|
||||
<SearchWrapper />
|
||||
{this.state.ngInjector && this.container && this.renderRoutes()}
|
||||
{this.state.ngInjector && this.renderRoutes()}
|
||||
{bodyRenderHooks.map((Hook, index) => (
|
||||
<Hook key={index.toString()} />
|
||||
))}
|
||||
|
@ -92,6 +92,9 @@ export class AngularApp {
|
||||
registerAngularDirectives();
|
||||
registerComponents();
|
||||
initAngularRoutingBridge();
|
||||
|
||||
// disable tool tip animation
|
||||
$.fn.tooltip.defaults.animation = false;
|
||||
}
|
||||
|
||||
useModule(module: angular.IModule) {
|
||||
|
15
public/app/angular/AngularRoot.tsx
Normal file
15
public/app/angular/AngularRoot.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export const AngularRoot = React.forwardRef<HTMLDivElement, {}>((props, ref) => {
|
||||
return (
|
||||
<div
|
||||
id="ngRoot"
|
||||
ref={ref}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: '<grafana-app ng-cloak></grafana-app>',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
AngularRoot.displayName = 'AngularRoot';
|
@ -5,27 +5,16 @@ import $ from 'jquery';
|
||||
|
||||
// Utils and servies
|
||||
import { colors } from '@grafana/ui';
|
||||
import {
|
||||
setDataSourceSrv,
|
||||
setLegacyAngularInjector,
|
||||
setLocationSrv,
|
||||
locationService,
|
||||
setAppEvents,
|
||||
setAngularLoader,
|
||||
} from '@grafana/runtime';
|
||||
import { setLegacyAngularInjector, setAppEvents, setAngularLoader } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { AngularLoader } from 'app/angular/services/AngularLoader';
|
||||
|
||||
// Types
|
||||
import { CoreEvents, AppEventEmitter, AppEventConsumer } from 'app/types';
|
||||
import { setLinkSrv, LinkSrv } from 'app/angular/panel/panellinks/link_srv';
|
||||
import { UtilSrv } from 'app/core/services/util_srv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { IRootScopeService, IAngularEvent, auto } from 'angular';
|
||||
import { AppEvent } from '@grafana/data';
|
||||
import { initGrafanaLive } from 'app/features/live';
|
||||
@ -39,22 +28,12 @@ export class GrafanaCtrl {
|
||||
utilSrv: UtilSrv,
|
||||
$rootScope: GrafanaRootScope,
|
||||
contextSrv: ContextSrv,
|
||||
linkSrv: LinkSrv,
|
||||
datasourceSrv: DatasourceSrv,
|
||||
dashboardSrv: DashboardSrv,
|
||||
angularLoader: AngularLoader,
|
||||
$injector: auto.IInjectorService
|
||||
) {
|
||||
// make angular loader service available to react components
|
||||
setAngularLoader(angularLoader);
|
||||
setDataSourceSrv(datasourceSrv);
|
||||
setLinkSrv(linkSrv);
|
||||
setDashboardSrv(dashboardSrv);
|
||||
setLegacyAngularInjector($injector);
|
||||
|
||||
datasourceSrv.init(config.datasources, config.defaultDatasource);
|
||||
|
||||
setLocationSrv(locationService);
|
||||
setAppEvents(appEvents);
|
||||
|
||||
initGrafanaLive();
|
||||
@ -63,8 +42,6 @@ export class GrafanaCtrl {
|
||||
$scope.contextSrv = contextSrv;
|
||||
$scope.appSubUrl = config.appSubUrl;
|
||||
$scope._ = _;
|
||||
|
||||
profiler.init(config, $rootScope);
|
||||
utilSrv.init();
|
||||
};
|
||||
|
||||
@ -116,8 +93,6 @@ export function grafanaAppDirective() {
|
||||
// see https://github.com/zenorocha/clipboard.js/issues/155
|
||||
$.fn.modal.Constructor.prototype.enforceFocus = () => {};
|
||||
|
||||
$('.preloader').remove();
|
||||
|
||||
appEvents.on(CoreEvents.toggleSidemenuHidden, () => {
|
||||
body.toggleClass('sidemenu-hidden');
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import coreModule from './core_module';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
|
||||
export class DeltaCtrl {
|
||||
observer: any;
|
||||
|
@ -31,5 +31,6 @@ import './components/info_popover';
|
||||
import './components/spectrum_picker';
|
||||
import './components/code_editor/code_editor';
|
||||
import './components/sql_part/sql_part_editor';
|
||||
import './GrafanaCtrl';
|
||||
|
||||
export { AngularApp } from './AngularApp';
|
||||
|
23
public/app/angular/lazyBootAngular.ts
Normal file
23
public/app/angular/lazyBootAngular.ts
Normal file
@ -0,0 +1,23 @@
|
||||
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,8 +1,15 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getLinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||
import coreModule from './core_module';
|
||||
import { AnnotationsSrv } from './services/annotations_srv';
|
||||
|
||||
export function registerComponents() {
|
||||
coreModule.factory('backendSrv', () => getBackendSrv());
|
||||
coreModule.factory('contextSrv', () => contextSrv);
|
||||
coreModule.factory('dashboardSrv', () => getDashboardSrv());
|
||||
coreModule.factory('datasourceSrv', () => getDataSourceSrv());
|
||||
coreModule.factory('linkSrv', () => getLinkSrv());
|
||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import coreModule from 'app/angular/core_module';
|
||||
import { assign } from 'lodash';
|
||||
|
||||
import { AngularComponent, AngularLoader as AngularLoaderInterface } from '@grafana/runtime';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
|
||||
export class AngularLoader implements AngularLoaderInterface {
|
||||
/** @ngInject */
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AnnotationsSrv } from '../annotations_srv';
|
||||
import { AnnotationsSrv } from './annotations_srv';
|
||||
|
||||
describe('AnnotationsSrv', () => {
|
||||
const annotationsSrv = new AnnotationsSrv();
|
@ -1,26 +1,9 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
import {
|
||||
AnnotationEvent,
|
||||
CoreApp,
|
||||
DataQueryRequest,
|
||||
DataSourceApi,
|
||||
deprecationWarning,
|
||||
rangeUtil,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { AnnotationEvent, deprecationWarning } from '@grafana/data';
|
||||
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import { AnnotationQueryOptions, AnnotationQueryResponse } from './types';
|
||||
import { standardAnnotationSupport } from './standardAnnotationSupport';
|
||||
import { runRequest } from '../query/state/runRequest';
|
||||
import { deleteAnnotation, saveAnnotation, updateAnnotation } from './api';
|
||||
import { deleteAnnotation, saveAnnotation, updateAnnotation } from 'app/features/annotations/api';
|
||||
import { AnnotationQueryOptions } from 'app/features/annotations/types';
|
||||
|
||||
let counter = 100;
|
||||
function getNextRequestId() {
|
||||
return 'AQ' + counter++;
|
||||
}
|
||||
/**
|
||||
* @deprecated AnnotationsSrv is deprecated in favor of DashboardQueryRunner
|
||||
*/
|
||||
@ -102,67 +85,3 @@ export class AnnotationsSrv {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export function executeAnnotationQuery(
|
||||
options: AnnotationQueryOptions,
|
||||
datasource: DataSourceApi,
|
||||
savedJsonAnno: any
|
||||
): Observable<AnnotationQueryResponse> {
|
||||
const processor = {
|
||||
...standardAnnotationSupport,
|
||||
...datasource.annotations,
|
||||
};
|
||||
|
||||
const annotation = processor.prepareAnnotation!(savedJsonAnno);
|
||||
if (!annotation) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
const query = processor.prepareQuery!(annotation);
|
||||
if (!query) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
// No more points than pixels
|
||||
const maxDataPoints = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
// Add interval to annotation queries
|
||||
const interval = rangeUtil.calculateInterval(options.range, maxDataPoints, datasource.interval);
|
||||
|
||||
const scopedVars: ScopedVars = {
|
||||
__interval: { text: interval.interval, value: interval.interval },
|
||||
__interval_ms: { text: interval.intervalMs.toString(), value: interval.intervalMs },
|
||||
__annotation: { text: annotation.name, value: annotation },
|
||||
};
|
||||
|
||||
const queryRequest: DataQueryRequest = {
|
||||
startTime: Date.now(),
|
||||
requestId: getNextRequestId(),
|
||||
range: options.range,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
...interval,
|
||||
app: CoreApp.Dashboard,
|
||||
|
||||
timezone: options.dashboard.timezone,
|
||||
|
||||
targets: [
|
||||
{
|
||||
...query,
|
||||
refId: 'Anno',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return runRequest(datasource, queryRequest).pipe(
|
||||
mergeMap((panelData) => {
|
||||
if (!panelData.series) {
|
||||
return of({ panelData, events: [] });
|
||||
}
|
||||
|
||||
return processor.processEvents!(annotation, panelData.series).pipe(map((events) => ({ panelData, events })));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
@ -2,7 +2,7 @@ import { extend } from 'lodash';
|
||||
import coreModule from 'app/angular/core_module';
|
||||
// @ts-ignore
|
||||
import Drop from 'tether-drop';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
|
||||
/** @ngInject */
|
||||
function popoverSrv(this: any, $compile: any, $rootScope: GrafanaRootScope, $timeout: any) {
|
||||
|
@ -27,16 +27,18 @@ import {
|
||||
import { arrayMove } from 'app/core/utils/arrayMove';
|
||||
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||
import {
|
||||
locationService,
|
||||
registerEchoBackend,
|
||||
setBackendSrv,
|
||||
setDataSourceSrv,
|
||||
setEchoSrv,
|
||||
setLocationSrv,
|
||||
setPanelRenderer,
|
||||
setQueryRunnerFactory,
|
||||
} from '@grafana/runtime';
|
||||
import { Echo } from './core/services/echo/Echo';
|
||||
import { reportPerformance } from './core/services/echo/EchoSrv';
|
||||
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
|
||||
import 'app/routes/GrafanaCtrl';
|
||||
import 'app/features/all';
|
||||
import { getScrollbarWidth, getStandardFieldConfigs } from '@grafana/ui';
|
||||
import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters';
|
||||
@ -47,7 +49,6 @@ import { setVariableQueryRunner, VariableQueryRunner } from './features/variable
|
||||
import { configureStore } from './store/configureStore';
|
||||
import { AppWrapper } from './AppWrapper';
|
||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||
import { AngularApp } from './angular';
|
||||
import { PanelRenderer } from './features/panel/components/PanelRenderer';
|
||||
import { QueryRunner } from './features/query/state/QueryRunner';
|
||||
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
|
||||
@ -59,6 +60,8 @@ import { ApplicationInsightsBackend } from './core/services/echo/backends/analyt
|
||||
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
|
||||
import { getAllOptionEditors } from './core/components/editors/registry';
|
||||
import { backendSrv } from './core/services/backend_srv';
|
||||
import { DatasourceSrv } from './features/plugins/datasource_srv';
|
||||
import { AngularApp } from './angular';
|
||||
|
||||
// add move to lodash for backward compatabilty with plugins
|
||||
// @ts-ignore
|
||||
@ -89,6 +92,7 @@ export class GrafanaApp {
|
||||
setLocale(config.bootData.user.locale);
|
||||
setWeekStart(config.bootData.user.weekStart);
|
||||
setPanelRenderer(PanelRenderer);
|
||||
setLocationSrv(locationService);
|
||||
setTimeZoneResolver(() => config.bootData.user.timezone);
|
||||
// Important that extensions are initialized before store
|
||||
initExtensions();
|
||||
@ -112,9 +116,12 @@ export class GrafanaApp {
|
||||
// intercept anchor clicks and forward it to custom history instead of relying on browser's history
|
||||
document.addEventListener('click', interceptLinkClicks);
|
||||
|
||||
// disable tool tip animation
|
||||
$.fn.tooltip.defaults.animation = false;
|
||||
// Init DataSourceSrv
|
||||
const dataSourceSrv = new DatasourceSrv();
|
||||
dataSourceSrv.init(config.datasources, config.defaultDatasource);
|
||||
setDataSourceSrv(dataSourceSrv);
|
||||
|
||||
// Init angular
|
||||
this.angularApp.init();
|
||||
|
||||
// Preload selected app plugins
|
||||
|
@ -1,19 +1,6 @@
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
|
||||
export class Profiler {
|
||||
panelsRendered = 0;
|
||||
enabled?: boolean = undefined;
|
||||
$rootScope?: GrafanaRootScope = undefined;
|
||||
window?: any = undefined;
|
||||
|
||||
init(config: any, $rootScope: GrafanaRootScope) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.window = window;
|
||||
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
renderingCompleted() {
|
||||
// add render counter to root scope
|
||||
@ -22,7 +9,7 @@ export class Profiler {
|
||||
|
||||
// this window variable is used by backend rendering tools to know
|
||||
// all panels have completed rendering
|
||||
this.window.panelsRendered = this.panelsRendered;
|
||||
(window as any).panelsRendered = this.panelsRendered;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,7 @@ import ReactDOM from 'react-dom';
|
||||
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
import { AngularModalProxy } from '../components/modals/AngularModalProxy';
|
||||
import { provideTheme } from '../utils/ConfigProvider';
|
||||
import {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import './annotations/all';
|
||||
import './plugins/all';
|
||||
import './dashboard';
|
||||
import './manage-dashboards';
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { AnnotationsSrv } from './annotations_srv';
|
||||
import { eventEditor } from './event_editor';
|
||||
import { EventManager } from './event_manager';
|
||||
import { annotationTooltipDirective } from './annotation_tooltip';
|
||||
export { AnnotationsSrv, eventEditor, EventManager, annotationTooltipDirective };
|
@ -7,7 +7,7 @@ import { Button, Icon, IconName, Spinner } from '@grafana/ui';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { standardAnnotationSupport } from '../standardAnnotationSupport';
|
||||
import { executeAnnotationQuery } from '../annotations_srv';
|
||||
import { executeAnnotationQuery } from '../executeAnnotationQuery';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { AnnotationQueryResponse } from '../types';
|
||||
import { AnnotationFieldMapper } from './AnnotationResultMapper';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { dedupAnnotations } from '../events_processing';
|
||||
import { dedupAnnotations } from './events_processing';
|
||||
|
||||
describe('Annotations deduplication', () => {
|
||||
it('should remove duplicated annotations', () => {
|
74
public/app/features/annotations/executeAnnotationQuery.ts
Normal file
74
public/app/features/annotations/executeAnnotationQuery.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
import { CoreApp, DataQueryRequest, DataSourceApi, rangeUtil, ScopedVars } from '@grafana/data';
|
||||
|
||||
import { AnnotationQueryOptions, AnnotationQueryResponse } from './types';
|
||||
import { standardAnnotationSupport } from './standardAnnotationSupport';
|
||||
import { runRequest } from '../query/state/runRequest';
|
||||
|
||||
let counter = 100;
|
||||
function getNextRequestId() {
|
||||
return 'AQ' + counter++;
|
||||
}
|
||||
|
||||
export function executeAnnotationQuery(
|
||||
options: AnnotationQueryOptions,
|
||||
datasource: DataSourceApi,
|
||||
savedJsonAnno: any
|
||||
): Observable<AnnotationQueryResponse> {
|
||||
const processor = {
|
||||
...standardAnnotationSupport,
|
||||
...datasource.annotations,
|
||||
};
|
||||
|
||||
const annotation = processor.prepareAnnotation!(savedJsonAnno);
|
||||
if (!annotation) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
const query = processor.prepareQuery!(annotation);
|
||||
if (!query) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
// No more points than pixels
|
||||
const maxDataPoints = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
// Add interval to annotation queries
|
||||
const interval = rangeUtil.calculateInterval(options.range, maxDataPoints, datasource.interval);
|
||||
|
||||
const scopedVars: ScopedVars = {
|
||||
__interval: { text: interval.interval, value: interval.interval },
|
||||
__interval_ms: { text: interval.intervalMs.toString(), value: interval.intervalMs },
|
||||
__annotation: { text: annotation.name, value: annotation },
|
||||
};
|
||||
|
||||
const queryRequest: DataQueryRequest = {
|
||||
startTime: Date.now(),
|
||||
requestId: getNextRequestId(),
|
||||
range: options.range,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
...interval,
|
||||
app: CoreApp.Dashboard,
|
||||
|
||||
timezone: options.dashboard.timezone,
|
||||
|
||||
targets: [
|
||||
{
|
||||
...query,
|
||||
refId: 'Anno',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return runRequest(datasource, queryRequest).pipe(
|
||||
mergeMap((panelData) => {
|
||||
if (!panelData.series) {
|
||||
return of({ panelData, events: [] });
|
||||
}
|
||||
|
||||
return processor.processEvents!(annotation, panelData.series).pipe(map((events) => ({ panelData, events })));
|
||||
})
|
||||
);
|
||||
}
|
@ -14,7 +14,7 @@ import { OptionPaneRenderProps } from './types';
|
||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||
import { DynamicConfigValueEditor } from './DynamicConfigValueEditor';
|
||||
import { getDataLinksVariableSuggestions } from 'app/angular/panel/panellinks/link_srv';
|
||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
import { OverrideCategoryTitle } from './OverrideCategoryTitle';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataLinksInlineEditor, Input, RadioButtonGroup, Select, Switch, TextArea } from '@grafana/ui';
|
||||
import { getPanelLinksVariableSuggestions } from 'app/angular/panel/panellinks/link_srv';
|
||||
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
import React from 'react';
|
||||
import { RepeatRowSelect } from '../RepeatRowSelect/RepeatRowSelect';
|
||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { StandardEditorContext, VariableSuggestionsScope } from '@grafana/data';
|
||||
import { get as lodashGet } from 'lodash';
|
||||
import { getDataLinksVariableSuggestions } from 'app/angular/panel/panellinks/link_srv';
|
||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
import { OptionPaneRenderProps } from './types';
|
||||
import { updateDefaultFieldConfigValue, setOptionImmutably } from './utils';
|
||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||
|
@ -2,7 +2,7 @@ import React, { FC } from 'react';
|
||||
import { Icon, IconName, Tooltip, useForceUpdate } from '@grafana/ui';
|
||||
import { sanitizeUrl } from '@grafana/data/src/text/sanitize';
|
||||
import { DashboardLinksDashboard } from './DashboardLinksDashboard';
|
||||
import { getLinkSrv } from '../../../../angular/panel/panellinks/link_srv';
|
||||
import { getLinkSrv } from '../../../panel/panellinks/link_srv';
|
||||
|
||||
import { DashboardModel } from '../../state';
|
||||
import { DashboardLink } from '../../state/DashboardModel';
|
||||
|
@ -2,7 +2,7 @@ import React, { useRef, useState, useLayoutEffect } from 'react';
|
||||
import { Icon, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getLinkSrv } from '../../../../angular/panel/panellinks/link_srv';
|
||||
import { getLinkSrv } from '../../../panel/panellinks/link_srv';
|
||||
import { DashboardLink } from '../../state/DashboardModel';
|
||||
import { DashboardSearchHit } from 'app/features/search/types';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
@ -45,5 +44,3 @@ export class HistorySrv {
|
||||
|
||||
const historySrv = new HistorySrv();
|
||||
export { historySrv };
|
||||
|
||||
coreModule.service('historySrv', HistorySrv);
|
||||
|
@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { getPanelLinksSupplier } from 'app/angular/panel/panellinks/linkSuppliers';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { PanelHeaderNotices } from './PanelHeaderNotices';
|
||||
import { PanelHeaderMenuTrigger } from './PanelHeaderMenuTrigger';
|
||||
import { PanelHeaderLoadingIndicator } from './PanelHeaderLoadingIndicator';
|
||||
|
@ -1,4 +1,3 @@
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { removePanel } from '../utils/panel';
|
||||
@ -67,8 +66,6 @@ export class DashboardSrv {
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('dashboardSrv', DashboardSrv);
|
||||
|
||||
//
|
||||
// Code below is to export the service to React components
|
||||
//
|
||||
@ -80,5 +77,8 @@ export function setDashboardSrv(instance: DashboardSrv) {
|
||||
}
|
||||
|
||||
export function getDashboardSrv(): DashboardSrv {
|
||||
if (!singletonInstance) {
|
||||
singletonInstance = new DashboardSrv();
|
||||
}
|
||||
return singletonInstance;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptions } from '../../../core/components/TraceToLogsSettings';
|
||||
import { LinkSrv, setLinkSrv } from '../../../angular/panel/panellinks/link_srv';
|
||||
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
|
||||
describe('createSpanLinkFactory', () => {
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
LinkModel,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { setLinkSrv } from '../../../angular/panel/panellinks/link_srv';
|
||||
import { setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { setContextSrv } from '../../../core/services/context_srv';
|
||||
|
||||
describe('getFieldLinksForExplore', () => {
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
SplitOpen,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { getLinkSrv } from '../../../angular/panel/panellinks/link_srv';
|
||||
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getFieldLinksSupplier } from './linkSuppliers';
|
||||
import { applyFieldOverrides, createTheme, DataFrameView, dateTime, FieldDisplay, toDataFrame } from '@grafana/data';
|
||||
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from './link_srv';
|
||||
import { TemplateSrv } from '../../../features/templating/template_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
|
||||
// We do not need more here and TimeSrv is hard to setup fully.
|
||||
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
@ -1,7 +1,6 @@
|
||||
import { chain } from 'lodash';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import {
|
||||
DataFrame,
|
||||
@ -15,7 +14,6 @@ import {
|
||||
KeyValue,
|
||||
LinkModel,
|
||||
locationUtil,
|
||||
PanelPlugin,
|
||||
ScopedVars,
|
||||
textUtil,
|
||||
urlUtil,
|
||||
@ -23,7 +21,7 @@ import {
|
||||
VariableSuggestion,
|
||||
VariableSuggestionsScope,
|
||||
} from '@grafana/data';
|
||||
import { getVariablesUrlParams } from '../../../features/variables/getAllVariableValuesForUrl';
|
||||
import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl';
|
||||
|
||||
const timeRangeVars = [
|
||||
{
|
||||
@ -242,20 +240,6 @@ export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: Data
|
||||
return [...seriesVars, ...fieldVars, ...valueVars, valueCalcVar, ...getPanelLinksVariableSuggestions()];
|
||||
};
|
||||
|
||||
export const getPanelOptionsVariableSuggestions = (plugin: PanelPlugin, data?: DataFrame[]): VariableSuggestion[] => {
|
||||
const dataVariables = plugin.meta.skipDataQuery ? [] : getDataFrameVars(data || []);
|
||||
return [
|
||||
...dataVariables, // field values
|
||||
...getTemplateSrv()
|
||||
.getVariables()
|
||||
.map((variable) => ({
|
||||
value: variable.name as string,
|
||||
label: variable.name,
|
||||
origin: VariableOrigin.Template,
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
export interface LinkService {
|
||||
getDataLinkUIModel: <T>(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: T) => LinkModel<T>;
|
||||
getAnchorInfo: (link: any) => any;
|
||||
@ -263,8 +247,6 @@ export interface LinkService {
|
||||
}
|
||||
|
||||
export class LinkSrv implements LinkService {
|
||||
constructor() {}
|
||||
|
||||
getLinkUrl(link: any) {
|
||||
let url = locationUtil.assureBaseUrl(getTemplateSrv().replace(link.url || ''));
|
||||
let params: { [key: string]: any } = {};
|
||||
@ -353,14 +335,15 @@ export class LinkSrv implements LinkService {
|
||||
}
|
||||
}
|
||||
|
||||
let singleton: LinkService;
|
||||
let singleton: LinkService | undefined;
|
||||
|
||||
export function setLinkSrv(srv: LinkService) {
|
||||
singleton = srv;
|
||||
}
|
||||
|
||||
export function getLinkSrv(): LinkService {
|
||||
if (!singleton) {
|
||||
singleton = new LinkSrv();
|
||||
}
|
||||
return singleton;
|
||||
}
|
||||
|
||||
coreModule.service('linkSrv', LinkSrv);
|
@ -1,5 +1,3 @@
|
||||
// Libraries
|
||||
import coreModule from 'app/angular/core_module';
|
||||
// Services & Utils
|
||||
import { importDataSourcePlugin } from './plugin_loader';
|
||||
import {
|
||||
@ -7,6 +5,7 @@ import {
|
||||
DataSourceSrv as DataSourceService,
|
||||
getDataSourceSrv as getDataSourceService,
|
||||
TemplateSrv,
|
||||
getTemplateSrv,
|
||||
} from '@grafana/runtime';
|
||||
// Types
|
||||
import {
|
||||
@ -17,8 +16,6 @@ import {
|
||||
DataSourceSelectItem,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { auto } from 'angular';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
// Pretend Datasource
|
||||
import {
|
||||
dataSource as expressionDatasource,
|
||||
@ -27,6 +24,8 @@ import {
|
||||
} from 'app/features/expressions/ExpressionDatasource';
|
||||
import { DataSourceVariableModel } from '../variables/types';
|
||||
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { getAngularInjector } from 'app/angular/lazyBootAngular';
|
||||
|
||||
export class DatasourceSrv implements DataSourceService {
|
||||
private datasources: Record<string, DataSourceApi> = {}; // UID
|
||||
@ -35,12 +34,7 @@ export class DatasourceSrv implements DataSourceService {
|
||||
private settingsMapById: Record<string, DataSourceInstanceSettings> = {};
|
||||
private defaultName = ''; // actually UID
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $injector: auto.IInjectorService,
|
||||
private $rootScope: GrafanaRootScope,
|
||||
private templateSrv: TemplateSrv
|
||||
) {}
|
||||
constructor(private templateSrv: TemplateSrv = getTemplateSrv()) {}
|
||||
|
||||
init(settingsMapByName: Record<string, DataSourceInstanceSettings>, defaultName: string) {
|
||||
this.datasources = {};
|
||||
@ -164,11 +158,15 @@ export class DatasourceSrv implements DataSourceService {
|
||||
|
||||
// If there is only one constructor argument it is instanceSettings
|
||||
const useAngular = dsPlugin.DataSourceClass.length !== 1;
|
||||
const instance: DataSourceApi = useAngular
|
||||
? this.$injector.instantiate(dsPlugin.DataSourceClass, {
|
||||
instanceSettings: dsConfig,
|
||||
})
|
||||
: new dsPlugin.DataSourceClass(dsConfig);
|
||||
let instance: DataSourceApi<any, any>;
|
||||
|
||||
if (useAngular) {
|
||||
instance = (await getAngularInjector()).instantiate(dsPlugin.DataSourceClass, {
|
||||
instanceSettings: dsConfig,
|
||||
});
|
||||
} else {
|
||||
instance = new dsPlugin.DataSourceClass(dsConfig);
|
||||
}
|
||||
|
||||
instance.components = dsPlugin.components;
|
||||
instance.meta = dsConfig.meta;
|
||||
@ -178,9 +176,7 @@ export class DatasourceSrv implements DataSourceService {
|
||||
this.datasources[instance.uid] = instance;
|
||||
return instance;
|
||||
} catch (err) {
|
||||
if (this.$rootScope) {
|
||||
this.$rootScope.appEvent(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]);
|
||||
}
|
||||
appEvents.emit(AppEvents.alertError, [dsConfig.name + ' plugin failed', err.toString()]);
|
||||
return Promise.reject({ message: `Datasource: ${key} was not found` });
|
||||
}
|
||||
}
|
||||
@ -321,5 +317,4 @@ export const getDatasourceSrv = (): DatasourceSrv => {
|
||||
return getDataSourceService() as DatasourceSrv;
|
||||
};
|
||||
|
||||
coreModule.service('datasourceSrv', DatasourceSrv);
|
||||
export default DatasourceSrv;
|
||||
|
@ -8,7 +8,7 @@ import { DataSourceApi, PanelEvents } from '@grafana/data';
|
||||
import { importDataSourcePlugin, importAppPlugin } from './plugin_loader';
|
||||
import { importPanelPlugin } from './importPanelPlugin';
|
||||
import DatasourceSrv from './datasource_srv';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
|
||||
/** @ngInject */
|
||||
function pluginDirectiveLoader(
|
||||
|
@ -3,7 +3,7 @@ import { find } from 'lodash';
|
||||
|
||||
import { getPluginSettings } from './PluginSettingsCache';
|
||||
import { PluginMeta, AppEvents } from '@grafana/data';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
import { promiseToDigest } from '../../angular/promiseToDigest';
|
||||
import { NavModelSrv } from 'app/angular/services/nav_model_srv';
|
||||
|
||||
|
@ -37,7 +37,7 @@ jest.mock('../plugin_loader', () => ({
|
||||
}));
|
||||
|
||||
describe('datasource_srv', () => {
|
||||
const dataSourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
|
||||
const dataSourceSrv = new DatasourceSrv(templateSrv);
|
||||
const dataSourceInit = {
|
||||
mmm: {
|
||||
type: 'test-db',
|
||||
|
@ -4,7 +4,7 @@ import { AnnotationsQueryRunner } from './AnnotationsQueryRunner';
|
||||
import { AnnotationQueryRunnerOptions } from './types';
|
||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||
import * as store from '../../../../store/store';
|
||||
import * as annotationsSrv from '../../../annotations/annotations_srv';
|
||||
import * as annotationsSrv from '../../../annotations/executeAnnotationQuery';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { toAsyncOfResult } from './testHelpers';
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { AnnotationEvent, DataSourceApi } from '@grafana/data';
|
||||
|
||||
import { AnnotationQueryRunner, AnnotationQueryRunnerOptions } from './types';
|
||||
import { PanelModel } from '../../../dashboard/state';
|
||||
import { executeAnnotationQuery } from '../../../annotations/annotations_srv';
|
||||
import { executeAnnotationQuery } from '../../../annotations/executeAnnotationQuery';
|
||||
import { handleAnnotationQueryRunnerError } from './utils';
|
||||
|
||||
export class AnnotationsQueryRunner implements AnnotationQueryRunner {
|
||||
|
@ -2,7 +2,7 @@ import { Subject, throwError } from 'rxjs';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
import { AnnotationsWorker } from './AnnotationsWorker';
|
||||
import * as annotationsSrv from '../../../annotations/annotations_srv';
|
||||
import * as annotationsSrv from '../../../annotations/executeAnnotationQuery';
|
||||
import { getDefaultOptions, LEGACY_DS_NAME, NEXT_GEN_DS_NAME, toAsyncOfResult } from './testHelpers';
|
||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||
import { createDashboardQueryRunner, setDashboardQueryRunnerFactory } from './DashboardQueryRunner';
|
||||
|
@ -3,7 +3,7 @@ import { delay } from 'rxjs/operators';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { AlertState, AlertStateInfo } from '@grafana/data';
|
||||
|
||||
import * as annotationsSrv from '../../../annotations/annotations_srv';
|
||||
import * as annotationsSrv from '../../../annotations/executeAnnotationQuery';
|
||||
import { getDefaultOptions, LEGACY_DS_NAME, NEXT_GEN_DS_NAME, toAsyncOfResult } from './testHelpers';
|
||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||
import { DashboardQueryRunner, DashboardQueryRunnerResult } from './types';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { DebugSection } from './DebugSection';
|
||||
import { mount } from 'enzyme';
|
||||
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from '../../../../angular/panel/panellinks/link_srv';
|
||||
import { getLinkSrv, LinkService, LinkSrv, setLinkSrv } from '../../../../features/panel/panellinks/link_srv';
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
// We do not need more here and TimeSrv is hard to setup fully.
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { isString, escape } from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/angular/core_module';
|
||||
import alertDef from '../alerting/state/alertDef';
|
||||
import { DashboardSrv } from '../dashboard/services/DashboardSrv';
|
||||
import alertDef from 'app/features/alerting/state/alertDef';
|
||||
import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
/** @ngInject */
|
@ -2,8 +2,8 @@ import { cloneDeep, isNumber } from 'lodash';
|
||||
import { coreModule } from 'app/angular/core_module';
|
||||
import { AnnotationEvent, dateTime } from '@grafana/data';
|
||||
import { MetricsPanelCtrl } from 'app/angular/panel/metrics_panel_ctrl';
|
||||
import { deleteAnnotation, saveAnnotation, updateAnnotation } from './api';
|
||||
import { getDashboardQueryRunner } from '../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../../features/annotations/api';
|
||||
import { getDashboardQueryRunner } from '../../../features/query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
|
||||
export class EventEditorCtrl {
|
||||
// @ts-ignore initialized through Angular not constructor
|
@ -16,7 +16,7 @@ import { coreModule } from 'app/angular/core_module';
|
||||
import GraphTooltip from './graph_tooltip';
|
||||
import { ThresholdManager } from './threshold_manager';
|
||||
import { TimeRegionManager } from './time_region_manager';
|
||||
import { EventManager } from 'app/features/annotations/all';
|
||||
import { EventManager } from './event_manager';
|
||||
import { convertToHistogramData } from './histogram';
|
||||
import { alignYLevel } from './align_yaxes';
|
||||
import config from 'app/core/config';
|
||||
@ -52,7 +52,7 @@ import {
|
||||
import { GraphContextMenuCtrl } from './GraphContextMenuCtrl';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
import { getFieldLinksSupplier } from 'app/angular/panel/panellinks/linkSuppliers';
|
||||
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { DashboardModel } from '../../../features/dashboard/state';
|
||||
import { isLegacyGraphHoverEvent } from './utils';
|
||||
|
||||
|
@ -2,6 +2,8 @@ import './graph';
|
||||
import './series_overrides_ctrl';
|
||||
import './thresholds_form';
|
||||
import './time_regions_form';
|
||||
import './annotation_tooltip';
|
||||
import './event_editor';
|
||||
|
||||
import template from './template';
|
||||
import { defaults, find, without } from 'lodash';
|
||||
|
@ -9,7 +9,7 @@ import { graphDirective, GraphElement } from '../graph';
|
||||
import { dateTime, EventBusSrv } from '@grafana/data';
|
||||
import { DashboardModel } from '../../../../features/dashboard/state';
|
||||
|
||||
jest.mock('app/features/annotations/all', () => ({
|
||||
jest.mock('../event_manager', () => ({
|
||||
EventManager: () => {
|
||||
return {
|
||||
on: () => {},
|
||||
|
@ -4,7 +4,7 @@ import config from 'app/core/config';
|
||||
import { angularMocks, sinon } from '../lib/common';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { RawTimeRange, PanelPluginMeta, dateMath } from '@grafana/data';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { GrafanaRootScope } from 'app/angular/GrafanaCtrl';
|
||||
|
||||
export function ControllerTestContext(this: any) {
|
||||
const self = this;
|
||||
|
Loading…
Reference in New Issue
Block a user