1// Libraries
2// eslint-disable-next-line lodash/import-scope
3import _ from 'lodash';
4import $ from 'jquery';
5
6// Utils and servies
7import { colors } from '@grafana/ui';
8import { setLegacyAngularInjector, setAppEvents, setAngularLoader } from '@grafana/runtime';
9import config from 'app/core/config';
10import coreModule from 'app/angular/core_module';
11import appEvents from 'app/core/app_events';
12import { AngularLoader } from 'app/angular/services/AngularLoader';
13
14// Types
15import { CoreEvents, AppEventEmitter, AppEventConsumer } from 'app/types';
16import { UtilSrv } from './services/UtilSrv';
17import { ContextSrv } from 'app/core/services/context_srv';
18import { IRootScopeService, IAngularEvent, auto } from 'angular';
19import { AppEvent } from '@grafana/data';
20import { initGrafanaLive } from 'app/features/live';
21
22export type GrafanaRootScope = IRootScopeService & AppEventEmitter & AppEventConsumer & { colors: string[] };
23
24export class GrafanaCtrl {
25  /** @ngInject */
26  constructor(
27    $scope: any,
28    utilSrv: UtilSrv,
29    $rootScope: GrafanaRootScope,
30    contextSrv: ContextSrv,
31    angularLoader: AngularLoader,
32    $injector: auto.IInjectorService
33  ) {
34    // make angular loader service available to react components
35    setAngularLoader(angularLoader);
36    setLegacyAngularInjector($injector);
37    setAppEvents(appEvents);
38
39    initGrafanaLive();
40
41    $scope.init = () => {
42      $scope.contextSrv = contextSrv;
43      $scope.appSubUrl = config.appSubUrl;
44      $scope._ = _;
45      utilSrv.init();
46    };
47
48    $rootScope.colors = colors;
49
50    $rootScope.onAppEvent = function <T>(
51      event: AppEvent<T> | string,
52      callback: (event: IAngularEvent, ...args: any[]) => void,
53      localScope?: any
54    ) {
55      let unbind;
56      if (typeof event === 'string') {
57        unbind = $rootScope.$on(event, callback);
58      } else {
59        unbind = $rootScope.$on(event.name, callback);
60      }
61
62      let callerScope = this;
63      if (callerScope.$id === 1 && !localScope) {
64        console.warn('warning rootScope onAppEvent called without localscope');
65      }
66      if (localScope) {
67        callerScope = localScope;
68      }
69      callerScope.$on('$destroy', unbind);
70    };
71
72    $rootScope.appEvent = <T>(event: AppEvent<T> | string, payload?: T | any) => {
73      if (typeof event === 'string') {
74        $rootScope.$emit(event, payload);
75        appEvents.emit(event, payload);
76      } else {
77        $rootScope.$emit(event.name, payload);
78        appEvents.emit(event, payload);
79      }
80    };
81
82    $scope.init();
83  }
84}
85
86/** @ngInject */
87export function grafanaAppDirective() {
88  return {
89    restrict: 'E',
90    controller: GrafanaCtrl,
91    link: (scope: IRootScopeService & AppEventEmitter, elem: JQuery) => {
92      const body = $('body');
93      // see https://github.com/zenorocha/clipboard.js/issues/155
94      $.fn.modal.Constructor.prototype.enforceFocus = () => {};
95
96      appEvents.on(CoreEvents.toggleSidemenuHidden, () => {
97        body.toggleClass('sidemenu-hidden');
98      });
99
100      // handle in active view state class
101      let lastActivity = new Date().getTime();
102      let activeUser = true;
103      const inActiveTimeLimit = 60 * 5000;
104
105      function checkForInActiveUser() {
106        if (!activeUser) {
107          return;
108        }
109        // only go to activity low mode on dashboard page
110        if (!body.hasClass('page-dashboard')) {
111          return;
112        }
113
114        if (new Date().getTime() - lastActivity > inActiveTimeLimit) {
115          activeUser = false;
116          body.addClass('view-mode--inactive');
117        }
118      }
119
120      function userActivityDetected() {
121        lastActivity = new Date().getTime();
122        if (!activeUser) {
123          activeUser = true;
124          body.removeClass('view-mode--inactive');
125        }
126      }
127
128      // mouse and keyboard is user activity
129      body.mousemove(userActivityDetected);
130      body.keydown(userActivityDetected);
131      // set useCapture = true to catch event here
132      document.addEventListener('wheel', userActivityDetected, { capture: true, passive: true });
133      // treat tab change as activity
134      document.addEventListener('visibilitychange', userActivityDetected);
135
136      // check every 2 seconds
137      setInterval(checkForInActiveUser, 2000);
138
139      // handle document clicks that should hide things
140      body.click((evt) => {
141        const target = $(evt.target);
142        if (target.parents().length === 0) {
143          return;
144        }
145
146        // ensure dropdown menu doesn't impact on z-index
147        body.find('.dropdown-menu-open').removeClass('dropdown-menu-open');
148
149        // for stuff that animates, slides out etc, clicking it needs to
150        // hide it right away
151        const clickAutoHide = target.closest('[data-click-hide]');
152        if (clickAutoHide.length) {
153          const clickAutoHideParent = clickAutoHide.parent();
154          clickAutoHide.detach();
155          setTimeout(() => {
156            clickAutoHideParent.append(clickAutoHide);
157          }, 100);
158        }
159
160        // hide popovers
161        const popover = elem.find('.popover');
162        if (popover.length > 0 && target.parents('.graph-legend').length === 0) {
163          popover.hide();
164        }
165      });
166    },
167  };
168}
169
170coreModule.directive('grafanaApp', grafanaAppDirective);
171