mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Routing NG: Replace Angular routing with react-router (#31463)
* Add router packages * Get react app root work instead of Angular one * Logger util * Patch Angular routing ($routeProvider, $routeParamsProvider) * Use react-router-dom history instead of separate dependency * Add test routes * Sidemenu - use Link instead of anchors * Patch Angular $location service (stub) * WIP: geting rid of $location provider from TimeSrv * Intercept anchor clicks to use history under the hood * Sync Redux location slice with history state * Make login/logout work * Debug routes for testing * Make force login work * Make sure query param change does not recreate page components * Hide side menu in specified locations * Make the dashboar route query parameters work, make panel edit menu work * Enable more routes * Fix side menu * Handle view modes * Disable playlist routes * Make SafeDynamicImport work again * Bring back router-debug * Separate redux location sync from route rendering * Refactor updateLocation to thunk and move force refresh(login) to it * Fixing init dashboard issue * Support switching between dashboards without an unmount of DashboardPage * More fixes for init dashboard and panel edit * More type fixes * Moving angular location wrapper out of main LocationService, and fixing typescript issues * Fixed last typescript errors * LocationService: Move to runtime and remove getLocationService and export singleston const instead (#31523) * Moving location service implementation to runtime and removing get function and making it a package const singleton * Added test that used locationService directly * removed unused import * AngularApp: Moving angular dependencies and the app boot out of the main app into it's own file (#31525) * Fixes angular panels by calling the monkey patch * Moving angular stuff to to it's own files * udpated * Fixing clicking on divs and spans inside anchor * Moving app notifications out of angular app and removing angular directive wrapper * Moving search from angular to react and removing angular search wrapper * Clean up, tried to remove the redux location wrapper but requires a big update for DashboardPage, so adding it back * Moving AppWrapper to root to limit circular dependencies (app/core -> app/routing and back) * Open and close search now works * Hide sidemenu when in kiosk mode * Restoring some keybindings like ESC key * Removed kiosk events and simplified it, just handled through updating URL * Fixing typescript errors * Simplified GrafanaRouteComponentProps and renamed to ContainerProps * renamed back * Changed AlertRuleList to use GrafanaRouteComponentProps and location.search passed to it * Removing the reloadOnSearch property, this is not needed now for react as react by default does not unmount components when only url match or query parmas change * SafeDynamicImport causing unmount un every search update, not sure how to fix yet * Fix signature for SafeDynamicImport so we do not create new route components on every route render * Removing the redux location wrapper as it was causing errors, and making dashboard page work with RouteProps (location, match) etc * Updating DashboardPage and SoloPanelPage to use match params and history location * Fixed DashboardPage tests * Fixing solo route tests * LocationService: Rename getCurrentLocation to just getLocation * do not intercept link clicks with target blank or self * Experimental useUrlParams hook * Update DataSourceSettingsPage to use router match params * fix links with urls that have no starting / to work like before * Fix forceLogin * Add queryParams to GrafanaRouteComponentProps * PanelEditor get rid of updateLocation and location state * Improve grafana route query params typing * Add getSearchObject to LocationService * Use DashboardPAge queryParams instead of location.search parsing * Fix DashboardPage typing * Fix some tests weirdness * Bring back KeyboardSrv * Fixes typescript issues * Team pages now use router match params * Get rid of from GrafanaRouteComponent props * Removed unnessary calls to getSearchObject when calling locationService.partial * Updated DashboardPage tests after queryParams was added * Fixing dashboard settings back * GrafanaRoute: Adding tests and remove use of global locationService * Fixing tests and typescript errors * Bring back kiosk modes and add tests * Fix TimeSrv tests * Fix typecheck errors * Fixing tests * Updated SideMenu test to react-testing and wrapped component in Router, and fixed issue importing createMemoryHistory * Get rid of routeChange event from TimeSrv from * Fixed TopSectionItem test * Trying to make basename work but failing * Update TopSectionItem snapshot * Fix TopSectionItem snapshot test * Fix API keys creation * Remove Angular dependencies from KeybindingSrv (#31617) * Remove Angular dependency from KeybindingsSrv * Fix tests and typecheck issues * basename is starting to work * Make dashboard save work * KeybindingSrv: Remove as angular service and no usage angular scope * So long bridge_srv, we won't miss you * Update snapshots * Dashboard: Refactoring ChangeTracker to use History api and no angular (#31653) * Dashboard: Refactoring ChangeTracker to use History api and no angular * Updated * Removed logging * fixed unit tests * updated snapshots * Mechanism for force reloading routes (#31683) * e2e: Fixes various things in e2e scenarios after router migration (#31685) * Explore: Update reading query params from router props and updating location via locationService (ReactRouter) (#31688) * RoutingNG: Initial explore redux location to router location migration * Updated explore Wrapper tests * Fixing more tests * remove loggin * rename back to make naming consistent * Fixing return to dashboard button * fixing navigation to explore from dashboard * updated routeProps * Updated tests * Make DashboardListPage work * Fixing navigation after add new data source, and fixes explore e2e * Fixing solo panel page * PluginsPage now works * RoutingNG: When parsing and rendering url search/query params preseve old logic of handling booleans and arrays (#31725) * RoutingNG: When parsing and rendering url search/query params preserve old logic of handling booleans and arrays * Fixed test * Make snapshots list work * fixed alert notification channel edit page * Simplify LocationService, did not need special handling for login or forceLogin as target _self on link already handles that * fixed UserAdminPage * fixed edit orgs page * Fixing LdapPage * fixing dashboard import * Fixed new folder page * Fixed data source dashboards page * fixing Folder permissions and folder settings page * fixing snapshot list page nav model * remove unused file * Added placeholder page for playlist * Moved browser compatability to index-template * Restored 404/default page * Fixed reset password page * Fixed SignUpInvited page * Fixing CreateTeam, Create user page, add panel widget * Restore browwser file to make tests happy * Fixed unit tests * Removed unused import * Replacing usage of updateLocation * Fixed test * Updating search filters to use history / location service for filters * remove unused file * AppRootPage fixed * Fixing test and search issue * Changes to support enterprise extensions * remove console.log * Removing more use of redux location * Fixed signup page * removed unused old angular controllers * Fixing bugs * one final bugfix * Removed location from redux state * Fixing ts issues and tests * Fixing test issue * fixing tests * Fixing tests * removed unused stuff * Fixed search test * Adding some doc comments * Routing NG: Angular location provider patch (#31773) * Patch Angulars $location provider * Update public/app/angular/bridgeReactAngularRouting.ts * Remove only test * Update tests, disable loggers in test env * Routing NG: remove $location provider usage (#31816) * Remove dashboard_loaders * Remove $location from Analytics service, track page views form GrafanaRoute * Remove NotificationsEditCtrl * Remove Angular dependencies from uploadDashboardDirective * Update public/app/features/dashboard/containers/DashboardPage.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update public/app/features/dashboard/containers/DashboardPage.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Remove unused test helpers (#31831) * Playlist react (#31829) * playlist list in react * Playlist start * Things started to work * Updated * Handle empty list * Fix ts * Fixes and kiosk mode stuff * Removed unused events * fixing ts issue * Another ts issue * Fixing tests Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> * fixed test * Update public/app/AppWrapper.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update public/app/AppWrapper.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Remove Angular dependency from DashboardLoaderSrv (#31863) Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
parent
def58f1f4a
commit
a55a272276
@ -3,7 +3,7 @@ import { e2e } from '@grafana/e2e';
|
|||||||
e2e.scenario({
|
e2e.scenario({
|
||||||
describeName: 'Explore',
|
describeName: 'Explore',
|
||||||
itName: 'Basic path through Explore.',
|
itName: 'Basic path through Explore.',
|
||||||
addScenarioDataSource: true,
|
addScenarioDataSource: false,
|
||||||
addScenarioDashBoard: false,
|
addScenarioDashBoard: false,
|
||||||
skipScenario: false,
|
skipScenario: false,
|
||||||
scenario: () => {
|
scenario: () => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
const PAGE_UNDER_TEST = '-Y-tnEDWk';
|
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||||
|
|
||||||
describe('Variables - Load options from Url', () => {
|
describe('Variables - Load options from Url', () => {
|
||||||
it('default options should be correct', () => {
|
it('default options should be correct', () => {
|
||||||
@ -58,7 +58,7 @@ describe('Variables - Load options from Url', () => {
|
|||||||
|
|
||||||
it('options set in url should load correct options', () => {
|
it('options set in url should load correct options', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-datacenter=B&var-server=BB&var-pod=BBB` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=B&var-server=BB&var-pod=BBB` });
|
||||||
e2e().server();
|
e2e().server();
|
||||||
e2e()
|
e2e()
|
||||||
.route({
|
.route({
|
||||||
@ -122,7 +122,7 @@ describe('Variables - Load options from Url', () => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-datacenter=X` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=X` });
|
||||||
e2e().server();
|
e2e().server();
|
||||||
e2e()
|
e2e()
|
||||||
.route({
|
.route({
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
const PAGE_UNDER_TEST = '-Y-tnEDWk';
|
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||||
|
|
||||||
describe('Variables - Add variable', () => {
|
describe('Variables - Add variable', () => {
|
||||||
it('query variable should be default and default fields should be correct', () => {
|
it('query variable should be default and default fields should be correct', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?editview=templating` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
|
||||||
|
|
||||||
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
|
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ describe('Variables - Add variable', () => {
|
|||||||
|
|
||||||
it('adding a single value query variable', () => {
|
it('adding a single value query variable', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?editview=templating` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
|
||||||
|
|
||||||
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
|
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ describe('Variables - Add variable', () => {
|
|||||||
|
|
||||||
it('adding a multi value query variable', () => {
|
it('adding a multi value query variable', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?editview=templating` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` });
|
||||||
|
|
||||||
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
|
e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click();
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
const PAGE_UNDER_TEST = '-Y-tnEDWk';
|
const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables';
|
||||||
|
|
||||||
describe('Variables - Set options from ui', () => {
|
describe('Variables - Set options from ui', () => {
|
||||||
it('clicking a value that is not part of dependents options should change these to All', () => {
|
it('clicking a value that is not part of dependents options should change these to All', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-datacenter=A&var-server=AA&var-pod=AAA` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` });
|
||||||
e2e().server();
|
e2e().server();
|
||||||
e2e()
|
e2e()
|
||||||
.route({
|
.route({
|
||||||
@ -26,6 +26,8 @@ describe('Variables - Set options from ui', () => {
|
|||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible');
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible');
|
||||||
|
|
||||||
|
e2e.components.LoadingIndicator.icon().should('have.length', 0);
|
||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 2);
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 2);
|
||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').eq(0).should('be.visible').click();
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').eq(0).should('be.visible').click();
|
||||||
@ -63,7 +65,7 @@ describe('Variables - Set options from ui', () => {
|
|||||||
|
|
||||||
it('adding a value that is not part of dependents options should add the new values dependant options', () => {
|
it('adding a value that is not part of dependents options should add the new values dependant options', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-datacenter=A&var-server=AA&var-pod=AAA` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` });
|
||||||
e2e().server();
|
e2e().server();
|
||||||
e2e()
|
e2e()
|
||||||
.route({
|
.route({
|
||||||
@ -84,6 +86,8 @@ describe('Variables - Set options from ui', () => {
|
|||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').scrollIntoView().should('be.visible');
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').scrollIntoView().should('be.visible');
|
||||||
|
|
||||||
|
e2e.components.LoadingIndicator.icon().should('have.length', 0);
|
||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click();
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click();
|
||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
|
||||||
@ -117,7 +121,7 @@ describe('Variables - Set options from ui', () => {
|
|||||||
it('removing a value that is part of dependents options should remove the new values dependant options', () => {
|
it('removing a value that is part of dependents options should remove the new values dependant options', () => {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({
|
e2e.flows.openDashboard({
|
||||||
uid: `${PAGE_UNDER_TEST}?var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`,
|
uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`,
|
||||||
});
|
});
|
||||||
e2e().server();
|
e2e().server();
|
||||||
e2e()
|
e2e()
|
||||||
@ -139,6 +143,8 @@ describe('Variables - Set options from ui', () => {
|
|||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible');
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible');
|
||||||
|
|
||||||
|
e2e.components.LoadingIndicator.icon().should('have.length', 0);
|
||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click();
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click();
|
||||||
|
|
||||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown()
|
||||||
|
@ -5,32 +5,16 @@ const PAGE_UNDER_TEST = 'AejrN1AMz';
|
|||||||
describe('TextBox - load options scenarios', function () {
|
describe('TextBox - load options scenarios', function () {
|
||||||
it('default options should be correct', function () {
|
it('default options should be correct', function () {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}/templating-textbox-e2e-scenarios?orgId=1` });
|
||||||
e2e().server();
|
|
||||||
e2e()
|
|
||||||
.route({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/dashboards/uid/${PAGE_UNDER_TEST}`,
|
|
||||||
})
|
|
||||||
.as('dash');
|
|
||||||
|
|
||||||
e2e().wait('@dash');
|
|
||||||
|
|
||||||
validateTextboxAndMarkup('default value');
|
validateTextboxAndMarkup('default value');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loading variable from url should be correct', function () {
|
it('loading variable from url should be correct', function () {
|
||||||
e2e.flows.login('admin', 'admin');
|
e2e.flows.login('admin', 'admin');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-text=not default value` });
|
e2e.flows.openDashboard({
|
||||||
e2e().server();
|
uid: `${PAGE_UNDER_TEST}/templating-textbox-e2e-scenarios?orgId=1&var-text=not default value`,
|
||||||
e2e()
|
});
|
||||||
.route({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/dashboards/uid/${PAGE_UNDER_TEST}`,
|
|
||||||
})
|
|
||||||
.as('dash');
|
|
||||||
|
|
||||||
e2e().wait('@dash');
|
|
||||||
|
|
||||||
validateTextboxAndMarkup('not default value');
|
validateTextboxAndMarkup('not default value');
|
||||||
});
|
});
|
||||||
@ -159,7 +143,7 @@ function copyExistingDashboard() {
|
|||||||
url: /\/api\/dashboards\/uid\/(?!AejrN1AMz)\w+/,
|
url: /\/api\/dashboards\/uid\/(?!AejrN1AMz)\w+/,
|
||||||
})
|
})
|
||||||
.as('load-dash');
|
.as('load-dash');
|
||||||
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?editview=settings&orgId=1` });
|
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}/templating-textbox-e2e-scenarios?orgId=1&editview=settings` });
|
||||||
|
|
||||||
e2e().wait('@dash-settings');
|
e2e().wait('@dash-settings');
|
||||||
|
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
"@types/enzyme": "3.10.5",
|
"@types/enzyme": "3.10.5",
|
||||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||||
"@types/file-saver": "2.0.1",
|
"@types/file-saver": "2.0.1",
|
||||||
|
"@types/history": "^4.7.8",
|
||||||
"@types/is-hotkey": "0.1.1",
|
"@types/is-hotkey": "0.1.1",
|
||||||
"@types/jest": "26.0.15",
|
"@types/jest": "26.0.15",
|
||||||
"@types/jquery": "3.3.38",
|
"@types/jquery": "3.3.38",
|
||||||
@ -107,6 +108,7 @@
|
|||||||
"@types/react-dom": "16.9.9",
|
"@types/react-dom": "16.9.9",
|
||||||
"@types/react-grid-layout": "1.1.1",
|
"@types/react-grid-layout": "1.1.1",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-select": "3.0.8",
|
"@types/react-select": "3.0.8",
|
||||||
"@types/react-test-renderer": "16.9.2",
|
"@types/react-test-renderer": "16.9.2",
|
||||||
"@types/react-transition-group": "4.4.0",
|
"@types/react-transition-group": "4.4.0",
|
||||||
@ -218,6 +220,7 @@
|
|||||||
"@types/react-virtualized-auto-sizer": "1.0.0",
|
"@types/react-virtualized-auto-sizer": "1.0.0",
|
||||||
"@types/uuid": "8.3.0",
|
"@types/uuid": "8.3.0",
|
||||||
"@welldone-software/why-did-you-render": "4.0.6",
|
"@welldone-software/why-did-you-render": "4.0.6",
|
||||||
|
"history": "4.10.1",
|
||||||
"abortcontroller-polyfill": "1.4.0",
|
"abortcontroller-polyfill": "1.4.0",
|
||||||
"angular": "1.8.2",
|
"angular": "1.8.2",
|
||||||
"angular-bindonce": "0.3.1",
|
"angular-bindonce": "0.3.1",
|
||||||
@ -269,6 +272,7 @@
|
|||||||
"react-popper": "2.2.4",
|
"react-popper": "2.2.4",
|
||||||
"react-redux": "7.2.0",
|
"react-redux": "7.2.0",
|
||||||
"react-reverse-portal": "^2.0.1",
|
"react-reverse-portal": "^2.0.1",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
"react-sizeme": "2.6.12",
|
"react-sizeme": "2.6.12",
|
||||||
"react-split-pane": "0.1.89",
|
"react-split-pane": "0.1.89",
|
||||||
"react-transition-group": "4.4.1",
|
"react-transition-group": "4.4.1",
|
||||||
|
@ -6,8 +6,10 @@ import { fallBackTreshold } from './thresholds';
|
|||||||
import { getScaleCalculator, ColorScaleValue } from './scale';
|
import { getScaleCalculator, ColorScaleValue } from './scale';
|
||||||
import { reduceField } from '../transformations/fieldReducer';
|
import { reduceField } from '../transformations/fieldReducer';
|
||||||
|
|
||||||
|
/** @beta */
|
||||||
export type FieldValueColorCalculator = (value: number, percent: number, Threshold?: Threshold) => string;
|
export type FieldValueColorCalculator = (value: number, percent: number, Threshold?: Threshold) => string;
|
||||||
|
|
||||||
|
/** @beta */
|
||||||
export interface FieldColorMode extends RegistryItem {
|
export interface FieldColorMode extends RegistryItem {
|
||||||
getCalculator: (field: Field, theme: GrafanaTheme) => FieldValueColorCalculator;
|
getCalculator: (field: Field, theme: GrafanaTheme) => FieldValueColorCalculator;
|
||||||
colors?: string[];
|
colors?: string[];
|
||||||
@ -15,6 +17,7 @@ export interface FieldColorMode extends RegistryItem {
|
|||||||
isByValue?: boolean;
|
isByValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -203,10 +206,12 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @beta */
|
||||||
export function getFieldColorModeForField(field: Field): FieldColorMode {
|
export function getFieldColorModeForField(field: Field): FieldColorMode {
|
||||||
return fieldColorModeRegistry.get(field.config.color?.mode ?? FieldColorModeId.Thresholds);
|
return fieldColorModeRegistry.get(field.config.color?.mode ?? FieldColorModeId.Thresholds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @beta */
|
||||||
export function getFieldColorMode(mode?: FieldColorModeId): FieldColorMode {
|
export function getFieldColorMode(mode?: FieldColorModeId): FieldColorMode {
|
||||||
return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds);
|
return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import isNumber from 'lodash/isNumber';
|
|||||||
|
|
||||||
type IndexComparer = (a: number, b: number) => number;
|
type IndexComparer = (a: number, b: number) => number;
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer => {
|
export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer => {
|
||||||
const values = field.values;
|
const values = field.values;
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export const timeComparer = (a: any, b: any): number => {
|
export const timeComparer = (a: any, b: any): number => {
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
return falsyComparer(a, b);
|
return falsyComparer(a, b);
|
||||||
@ -42,10 +44,12 @@ export const timeComparer = (a: any, b: any): number => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export const numericComparer = (a: number, b: number): number => {
|
export const numericComparer = (a: number, b: number): number => {
|
||||||
return a - b;
|
return a - b;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export const stringComparer = (a: string, b: string): number => {
|
export const stringComparer = (a: string, b: string): number => {
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
return falsyComparer(a, b);
|
return falsyComparer(a, b);
|
||||||
|
@ -66,9 +66,7 @@ export const customFieldRegistry: FieldConfigOptionsRegistry = new Registry<Fiel
|
|||||||
});
|
});
|
||||||
|
|
||||||
locationUtil.initialize({
|
locationUtil.initialize({
|
||||||
getConfig: () => {
|
config: { appSubUrl: '/subUrl' } as any,
|
||||||
return { appSubUrl: '/subUrl' } as any;
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
buildParamsFromVariables: () => {},
|
buildParamsFromVariables: () => {},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -529,7 +527,7 @@ describe('setDynamicConfigValue', () => {
|
|||||||
describe('getLinksSupplier', () => {
|
describe('getLinksSupplier', () => {
|
||||||
it('will replace variables in url and title of the data link', () => {
|
it('will replace variables in url and title of the data link', () => {
|
||||||
locationUtil.initialize({
|
locationUtil.initialize({
|
||||||
getConfig: () => ({} as any),
|
config: {} as any,
|
||||||
buildParamsFromVariables: (() => {}) as any,
|
buildParamsFromVariables: (() => {}) as any,
|
||||||
getTimeRangeForUrl: (() => {}) as any,
|
getTimeRangeForUrl: (() => {}) as any,
|
||||||
});
|
});
|
||||||
@ -573,7 +571,7 @@ describe('getLinksSupplier', () => {
|
|||||||
|
|
||||||
it('handles internal links', () => {
|
it('handles internal links', () => {
|
||||||
locationUtil.initialize({
|
locationUtil.initialize({
|
||||||
getConfig: () => ({ appSubUrl: '' } as any),
|
config: { appSubUrl: '' } as any,
|
||||||
buildParamsFromVariables: (() => {}) as any,
|
buildParamsFromVariables: (() => {}) as any,
|
||||||
getTimeRangeForUrl: (() => {}) as any,
|
getTimeRangeForUrl: (() => {}) as any,
|
||||||
});
|
});
|
||||||
|
@ -3,15 +3,14 @@ import { locationUtil } from './location';
|
|||||||
describe('locationUtil', () => {
|
describe('locationUtil', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
locationUtil.initialize({
|
locationUtil.initialize({
|
||||||
getConfig: () => {
|
config: { appSubUrl: '/subUrl' } as any,
|
||||||
return { appSubUrl: '/subUrl' } as any;
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
buildParamsFromVariables: () => {},
|
buildParamsFromVariables: () => {},
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
getTimeRangeForUrl: () => {},
|
getTimeRangeForUrl: () => {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('With /subUrl as appSubUrl', () => {
|
describe('With /subUrl as appSubUrl', () => {
|
||||||
it('/subUrl should be stripped', () => {
|
it('/subUrl should be stripped', () => {
|
||||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
|
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
|
||||||
|
@ -2,7 +2,7 @@ import { GrafanaConfig, RawTimeRange, ScopedVars } from '../types';
|
|||||||
import { urlUtil } from './url';
|
import { urlUtil } from './url';
|
||||||
import { textUtil } from '../text';
|
import { textUtil } from '../text';
|
||||||
|
|
||||||
let grafanaConfig: () => GrafanaConfig;
|
let grafanaConfig: GrafanaConfig = { appSubUrl: '' } as any;
|
||||||
let getTimeRangeUrlParams: () => RawTimeRange;
|
let getTimeRangeUrlParams: () => RawTimeRange;
|
||||||
let getVariablesUrlParams: (params?: Record<string, any>, scopedVars?: ScopedVars) => string;
|
let getVariablesUrlParams: (params?: Record<string, any>, scopedVars?: ScopedVars) => string;
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ let getVariablesUrlParams: (params?: Record<string, any>, scopedVars?: ScopedVar
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
const stripBaseFromUrl = (url: string): string => {
|
const stripBaseFromUrl = (url: string): string => {
|
||||||
const appSubUrl = grafanaConfig ? grafanaConfig().appSubUrl : '';
|
const appSubUrl = grafanaConfig.appSubUrl ?? '';
|
||||||
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
||||||
const urlWithoutBase =
|
const urlWithoutBase =
|
||||||
url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url;
|
url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url;
|
||||||
@ -27,13 +27,13 @@ const stripBaseFromUrl = (url: string): string => {
|
|||||||
*/
|
*/
|
||||||
const assureBaseUrl = (url: string): string => {
|
const assureBaseUrl = (url: string): string => {
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
return `${grafanaConfig ? grafanaConfig().appSubUrl : ''}${stripBaseFromUrl(url)}`;
|
return `${grafanaConfig.appSubUrl}${stripBaseFromUrl(url)}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface LocationUtilDependencies {
|
interface LocationUtilDependencies {
|
||||||
getConfig: () => GrafanaConfig;
|
config: GrafanaConfig;
|
||||||
getTimeRangeForUrl: () => RawTimeRange;
|
getTimeRangeForUrl: () => RawTimeRange;
|
||||||
buildParamsFromVariables: (params: any, scopedVars?: ScopedVars) => string;
|
buildParamsFromVariables: (params: any, scopedVars?: ScopedVars) => string;
|
||||||
}
|
}
|
||||||
@ -46,8 +46,8 @@ export const locationUtil = {
|
|||||||
* @param getTimeRangeForUrl
|
* @param getTimeRangeForUrl
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
initialize: ({ getConfig, buildParamsFromVariables, getTimeRangeForUrl }: LocationUtilDependencies) => {
|
initialize: ({ config, buildParamsFromVariables, getTimeRangeForUrl }: LocationUtilDependencies) => {
|
||||||
grafanaConfig = getConfig;
|
grafanaConfig = config;
|
||||||
getTimeRangeUrlParams = getTimeRangeForUrl;
|
getTimeRangeUrlParams = getTimeRangeForUrl;
|
||||||
getVariablesUrlParams = buildParamsFromVariables;
|
getVariablesUrlParams = buildParamsFromVariables;
|
||||||
},
|
},
|
||||||
@ -68,6 +68,6 @@ export const locationUtil = {
|
|||||||
return urlUtil.toUrlParams(params);
|
return urlUtil.toUrlParams(params);
|
||||||
},
|
},
|
||||||
processUrl: (url: string) => {
|
processUrl: (url: string) => {
|
||||||
return grafanaConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
return grafanaConfig.disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -23,3 +23,25 @@ describe('toUrlParams', () => {
|
|||||||
expect(url).toBe('server=:@');
|
expect(url).toBe('server=:@');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseKeyValue', () => {
|
||||||
|
it('should parse url search params to object', () => {
|
||||||
|
const obj = urlUtil.parseKeyValue('param=value¶m2=value2&kiosk');
|
||||||
|
expect(obj).toEqual({ param: 'value', param2: 'value2', kiosk: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse same url key multiple times to array', () => {
|
||||||
|
const obj = urlUtil.parseKeyValue('servers=A&servers=B');
|
||||||
|
expect(obj).toEqual({ servers: ['A', 'B'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse numeric params', () => {
|
||||||
|
const obj = urlUtil.parseKeyValue('num1=12&num2=12.2');
|
||||||
|
expect(obj).toEqual({ num1: 12, num2: 12.2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse boolean params', () => {
|
||||||
|
const obj = urlUtil.parseKeyValue('bool1&bool2=true&bool3=false');
|
||||||
|
expect(obj).toEqual({ bool1: true, bool2: true, bool3: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
* @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
|
* @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
import { ExploreUrlState } from '../types/explore';
|
import { ExploreUrlState } from '../types/explore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,11 +126,69 @@ function getUrlSearchParams() {
|
|||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an escaped url query string into key-value pairs.
|
||||||
|
* Attribution: Code dervived from https://github.com/angular/angular.js/master/src/Angular.js#L1396
|
||||||
|
* @returns {Object.<string,boolean|Array>}
|
||||||
|
*/
|
||||||
|
export function parseKeyValue(keyValue: string) {
|
||||||
|
var obj: any = {};
|
||||||
|
const parts = (keyValue || '').split('&');
|
||||||
|
|
||||||
|
for (let keyValue of parts) {
|
||||||
|
let splitPoint: number | undefined;
|
||||||
|
let key: string | undefined;
|
||||||
|
let val: string | undefined | boolean;
|
||||||
|
|
||||||
|
if (keyValue) {
|
||||||
|
key = keyValue = keyValue.replace(/\+/g, '%20');
|
||||||
|
splitPoint = keyValue.indexOf('=');
|
||||||
|
|
||||||
|
if (splitPoint !== -1) {
|
||||||
|
key = keyValue.substring(0, splitPoint);
|
||||||
|
val = keyValue.substring(splitPoint + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = tryDecodeURIComponent(key);
|
||||||
|
|
||||||
|
if (key !== undefined) {
|
||||||
|
val = val !== undefined ? tryDecodeURIComponent(val as string) : true;
|
||||||
|
|
||||||
|
let parsedVal: any;
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
parsedVal = val === 'true' || val === 'false' ? val === 'true' : _.toNumber(val);
|
||||||
|
} else {
|
||||||
|
parsedVal = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obj.hasOwnProperty(key)) {
|
||||||
|
obj[key] = isNaN(parsedVal) ? val : parsedVal;
|
||||||
|
} else if (Array.isArray(obj[key])) {
|
||||||
|
obj[key].push(val);
|
||||||
|
} else {
|
||||||
|
obj[key] = [obj[key], isNaN(parsedVal) ? val : parsedVal];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryDecodeURIComponent(value: string): string | undefined {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const urlUtil = {
|
export const urlUtil = {
|
||||||
renderUrl,
|
renderUrl,
|
||||||
toUrlParams,
|
toUrlParams,
|
||||||
appendQueryToUrl,
|
appendQueryToUrl,
|
||||||
getUrlSearchParams,
|
getUrlSearchParams,
|
||||||
|
parseKeyValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
|
||||||
|
@ -178,6 +178,9 @@ export const Components = {
|
|||||||
dropDown: 'Dashboard link dropdown',
|
dropDown: 'Dashboard link dropdown',
|
||||||
link: 'Dashboard link',
|
link: 'Dashboard link',
|
||||||
},
|
},
|
||||||
|
LoadingIndicator: {
|
||||||
|
icon: 'Loading indicator',
|
||||||
|
},
|
||||||
CallToActionCard: {
|
CallToActionCard: {
|
||||||
button: (name: string) => `Call to action button ${name}`,
|
button: (name: string) => `Call to action button ${name}`,
|
||||||
},
|
},
|
||||||
|
@ -34,7 +34,11 @@ export const Pages = {
|
|||||||
},
|
},
|
||||||
Dashboard: {
|
Dashboard: {
|
||||||
url: (uid: string) => `/d/${uid}`,
|
url: (uid: string) => `/d/${uid}`,
|
||||||
|
DashNav: {
|
||||||
|
nav: 'Dashboard navigation',
|
||||||
|
},
|
||||||
SubMenu: {
|
SubMenu: {
|
||||||
|
submenu: 'Dashboard submenu',
|
||||||
submenuItem: 'Dashboard template variables submenu item',
|
submenuItem: 'Dashboard template variables submenu item',
|
||||||
submenuItemLabels: (item: string) => `Dashboard template variables submenu Label ${item}`,
|
submenuItemLabels: (item: string) => `Dashboard template variables submenu Label ${item}`,
|
||||||
submenuItemValueDropDownValueLinkTexts: (item: string) =>
|
submenuItemValueDropDownValueLinkTexts: (item: string) =>
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
"@grafana/data": "7.5.0-pre.0",
|
"@grafana/data": "7.5.0-pre.0",
|
||||||
"@grafana/ui": "7.5.0-pre.0",
|
"@grafana/ui": "7.5.0-pre.0",
|
||||||
"systemjs": "0.20.19",
|
"systemjs": "0.20.19",
|
||||||
"systemjs-plugin-css": "0.1.37"
|
"systemjs-plugin-css": "0.1.37",
|
||||||
|
"history": "4.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||||
@ -34,6 +35,7 @@
|
|||||||
"@types/jest": "26.0.15",
|
"@types/jest": "26.0.15",
|
||||||
"@types/rollup-plugin-visualizer": "2.6.0",
|
"@types/rollup-plugin-visualizer": "2.6.0",
|
||||||
"@types/systemjs": "^0.20.6",
|
"@types/systemjs": "^0.20.6",
|
||||||
|
"@types/history": "^4.7.8",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"pretty-format": "25.1.0",
|
"pretty-format": "25.1.0",
|
||||||
"rollup": "2.33.3",
|
"rollup": "2.33.3",
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import { locationService } from './LocationService';
|
||||||
|
|
||||||
|
describe('LocationService', () => {
|
||||||
|
describe('getSearchObject', () => {
|
||||||
|
it('returns query string as object', () => {
|
||||||
|
locationService.push('/test?query1=false&query2=123&query3=text');
|
||||||
|
|
||||||
|
expect(locationService.getSearchObject()).toEqual({
|
||||||
|
query1: false,
|
||||||
|
query2: 123,
|
||||||
|
query3: 'text',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns keys added multiple times as an array', () => {
|
||||||
|
locationService.push('/test?servers=A&servers=B&servers=C');
|
||||||
|
|
||||||
|
expect(locationService.getSearchObject()).toEqual({
|
||||||
|
servers: ['A', 'B', 'C'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('partial', () => {
|
||||||
|
it('should handle removing params and updating', () => {
|
||||||
|
locationService.push('/test?query1=false&query2=123&query3=text');
|
||||||
|
locationService.partial({ query1: null, query2: 'update' });
|
||||||
|
|
||||||
|
expect(locationService.getLocation().search).toBe('?query2=update&query3=text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle array values', () => {
|
||||||
|
locationService.push('/');
|
||||||
|
locationService.partial({ servers: ['A', 'B', 'C'] });
|
||||||
|
|
||||||
|
expect(locationService.getLocation().search).toBe('?servers=A&servers=B&servers=C');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
171
packages/grafana-runtime/src/services/LocationService.ts
Normal file
171
packages/grafana-runtime/src/services/LocationService.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { UrlQueryMap, urlUtil } from '@grafana/data';
|
||||||
|
import * as H from 'history';
|
||||||
|
import { LocationUpdate } from './LocationSrv';
|
||||||
|
import { createLogger } from '@grafana/ui';
|
||||||
|
import { config } from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
* A wrapper to help work with browser location and history
|
||||||
|
*/
|
||||||
|
export interface LocationService {
|
||||||
|
partial: (query: Record<string, any>, replace?: boolean) => void;
|
||||||
|
push: (location: H.Path | H.LocationDescriptor<any>) => void;
|
||||||
|
replace: (location: H.Path | H.LocationDescriptor<any>, forceRouteReload?: boolean) => void;
|
||||||
|
reload: () => void;
|
||||||
|
getLocation: () => H.Location;
|
||||||
|
getHistory: () => H.History;
|
||||||
|
getSearch: () => URLSearchParams;
|
||||||
|
getSearchObject: () => UrlQueryMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is from the old LocationSrv interface
|
||||||
|
* @deprecated use partial, push or replace instead */
|
||||||
|
update: (update: LocationUpdate) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export class HistoryWrapper implements LocationService {
|
||||||
|
private readonly history: H.History;
|
||||||
|
|
||||||
|
constructor(history?: H.History) {
|
||||||
|
// If no history passed create an in memory one if being called from test
|
||||||
|
this.history =
|
||||||
|
history || process.env.NODE_ENV === 'test'
|
||||||
|
? H.createMemoryHistory({ initialEntries: ['/'] })
|
||||||
|
: H.createBrowserHistory({ basename: config.appSubUrl ?? '/' });
|
||||||
|
|
||||||
|
// For debugging purposes the location service is attached to global _debug variable
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// @ts-ignore
|
||||||
|
let debugGlobal = window['_debug'];
|
||||||
|
if (debugGlobal) {
|
||||||
|
debugGlobal = {
|
||||||
|
...debugGlobal,
|
||||||
|
location: this,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
debugGlobal = {
|
||||||
|
location: this,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
window['_debug'] = debugGlobal;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.partial = this.partial.bind(this);
|
||||||
|
this.push = this.push.bind(this);
|
||||||
|
this.replace = this.replace.bind(this);
|
||||||
|
this.getSearch = this.getSearch.bind(this);
|
||||||
|
this.getHistory = this.getHistory.bind(this);
|
||||||
|
this.getLocation = this.getLocation.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistory() {
|
||||||
|
return this.history;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearch() {
|
||||||
|
return new URLSearchParams(this.history.location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial(query: Record<string, any>, replace?: boolean) {
|
||||||
|
const currentLocation = this.history.location;
|
||||||
|
const newQuery = this.getSearchObject();
|
||||||
|
|
||||||
|
for (const key of Object.keys(query)) {
|
||||||
|
// removing params with null | undefined
|
||||||
|
if (query[key] === null || query[key] === undefined) {
|
||||||
|
delete newQuery[key];
|
||||||
|
} else {
|
||||||
|
newQuery[key] = query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUrl = urlUtil.renderUrl(currentLocation.pathname, newQuery);
|
||||||
|
|
||||||
|
if (replace) {
|
||||||
|
this.history.replace(updatedUrl);
|
||||||
|
} else {
|
||||||
|
this.history.push(updatedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push(location: H.Path | H.LocationDescriptor) {
|
||||||
|
this.history.push(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(location: H.Path | H.LocationDescriptor, forceRouteReload?: boolean) {
|
||||||
|
const state = forceRouteReload ? { forceRouteReload: true } : undefined;
|
||||||
|
|
||||||
|
if (typeof location === 'string') {
|
||||||
|
this.history.replace(location, state);
|
||||||
|
} else {
|
||||||
|
this.history.replace({
|
||||||
|
...location,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
this.history.replace({
|
||||||
|
...this.history.location,
|
||||||
|
state: { forceRouteReload: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocation() {
|
||||||
|
return this.history.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchObject() {
|
||||||
|
return locationSearchToObject(this.history.location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @depecreated */
|
||||||
|
update(options: LocationUpdate) {
|
||||||
|
if (options.partial && options.query) {
|
||||||
|
this.partial(options.query, options.partial);
|
||||||
|
} else if (options.replace) {
|
||||||
|
this.replace(options.path!);
|
||||||
|
} else {
|
||||||
|
this.push(options.path!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
* Parses a location search string to an object
|
||||||
|
* */
|
||||||
|
export function locationSearchToObject(search: string | number): UrlQueryMap {
|
||||||
|
let queryString = typeof search === 'number' ? String(search) : search;
|
||||||
|
|
||||||
|
if (queryString.length > 0) {
|
||||||
|
if (queryString.startsWith('?')) {
|
||||||
|
return urlUtil.parseKeyValue(queryString.substring(1));
|
||||||
|
}
|
||||||
|
return urlUtil.parseKeyValue(queryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export let locationService: LocationService = new HistoryWrapper();
|
||||||
|
|
||||||
|
/** @internal
|
||||||
|
* Used for tests only
|
||||||
|
*/
|
||||||
|
export const setLocationService = (location: LocationService) => {
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
throw new Error('locationService can be only overriden in test environment');
|
||||||
|
}
|
||||||
|
locationService = location;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const navigationLogger = createLogger('Router');
|
@ -6,3 +6,4 @@ export * from './EchoSrv';
|
|||||||
export * from './templateSrv';
|
export * from './templateSrv';
|
||||||
export * from './legacyAngularInjector';
|
export * from './legacyAngularInjector';
|
||||||
export * from './live';
|
export * from './live';
|
||||||
|
export * from './LocationService';
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"@visx/scale": "1.4.0",
|
"@visx/scale": "1.4.0",
|
||||||
"@visx/shape": "1.4.0",
|
"@visx/shape": "1.4.0",
|
||||||
"@visx/tooltip": "1.3.0",
|
"@visx/tooltip": "1.3.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
"d3": "5.15.0",
|
"d3": "5.15.0",
|
||||||
"emotion": "10.0.27",
|
"emotion": "10.0.27",
|
||||||
@ -89,6 +90,7 @@
|
|||||||
"@storybook/addon-storysource": "6.1.15",
|
"@storybook/addon-storysource": "6.1.15",
|
||||||
"@storybook/react": "6.1.15",
|
"@storybook/react": "6.1.15",
|
||||||
"@storybook/theming": "6.1.15",
|
"@storybook/theming": "6.1.15",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/classnames": "2.2.7",
|
"@types/classnames": "2.2.7",
|
||||||
"@types/common-tags": "^1.8.0",
|
"@types/common-tags": "^1.8.0",
|
||||||
"@types/d3": "5.7.2",
|
"@types/d3": "5.7.2",
|
||||||
|
20
packages/grafana-ui/src/components/Link/Link.tsx
Normal file
20
packages/grafana-ui/src/components/Link/Link.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { locationUtil, textUtil } from '@grafana/data';
|
||||||
|
import React, { AnchorHTMLAttributes, forwardRef } from 'react';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface Props extends AnchorHTMLAttributes<HTMLAnchorElement> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const Link = forwardRef<HTMLAnchorElement, Props>(({ href, children, ...rest }, ref) => {
|
||||||
|
const validUrl = locationUtil.stripBaseFromUrl(textUtil.sanitizeUrl(href ?? ''));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterLink ref={ref as React.Ref<HTMLAnchorElement>} to={validUrl} {...rest}>
|
||||||
|
{children}
|
||||||
|
</RouterLink>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Link.displayName = 'Link';
|
@ -144,6 +144,7 @@ export { Button, LinkButton, ButtonVariant, ToolbarButton, ButtonGroup, ToolbarB
|
|||||||
export { ValuePicker } from './ValuePicker/ValuePicker';
|
export { ValuePicker } from './ValuePicker/ValuePicker';
|
||||||
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
|
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
|
||||||
export { getFormStyles } from './Forms/getFormStyles';
|
export { getFormStyles } from './Forms/getFormStyles';
|
||||||
|
export { Link } from './Link/Link';
|
||||||
|
|
||||||
export { Label } from './Forms/Label';
|
export { Label } from './Forms/Label';
|
||||||
export { Field } from './Forms/Field';
|
export { Field } from './Forms/Field';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DataFrame, dateTime, FieldType } from '@grafana/data';
|
import { DataFrame, dateTime, FieldType } from '@grafana/data';
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import { AlignedData, Options } from 'uplot';
|
import { AlignedData, Options } from 'uplot';
|
||||||
import { PlotPlugin, PlotProps } from './types';
|
import { PlotPlugin, PlotProps } from './types';
|
||||||
|
import { createLogger } from '../../utils/logger';
|
||||||
|
|
||||||
const LOGGING_ENABLED = false;
|
const LOGGING_ENABLED = false;
|
||||||
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
|
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
|
||||||
@ -53,15 +53,4 @@ export function preparePlotData(frame: DataFrame): AlignedData {
|
|||||||
// Dev helpers
|
// Dev helpers
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const throttledLog = throttle((...t: any[]) => {
|
export const pluginLog = createLogger('uPlot Plugin', LOGGING_ENABLED);
|
||||||
console.log(...t);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export function pluginLog(id: string, throttle = false, ...t: any[]) {
|
|
||||||
if (process.env.NODE_ENV === 'production' || !LOGGING_ENABLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fn = throttle ? throttledLog : console.log;
|
|
||||||
fn(`[Plugin: ${id}]: `, ...t);
|
|
||||||
}
|
|
||||||
|
@ -10,3 +10,4 @@ export { default as ansicolor } from './ansicolor';
|
|||||||
import * as DOMUtil from './dom'; // includes Element.closest polyfill
|
import * as DOMUtil from './dom'; // includes Element.closest polyfill
|
||||||
export { DOMUtil };
|
export { DOMUtil };
|
||||||
export { renderOrCallToRender } from './renderOrCallToRender';
|
export { renderOrCallToRender } from './renderOrCallToRender';
|
||||||
|
export { createLogger } from './logger';
|
||||||
|
15
packages/grafana-ui/src/utils/logger.ts
Normal file
15
packages/grafana-ui/src/utils/logger.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
const throttledLog = throttle((...t: any[]) => {
|
||||||
|
console.log(...t);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const createLogger = (name: string, enable = true) => (id: string, throttle = false, ...t: any[]) => {
|
||||||
|
if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test' || !enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fn = throttle ? throttledLog : console.log;
|
||||||
|
fn(`[${name}: ${id}]: `, ...t);
|
||||||
|
};
|
112
public/app/AppWrapper.tsx
Normal file
112
public/app/AppWrapper.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Router, Route, Redirect, Switch } from 'react-router-dom';
|
||||||
|
import { config, locationService, navigationLogger } from '@grafana/runtime';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { store } from 'app/store/store';
|
||||||
|
import { ErrorBoundaryAlert, ModalRoot, ModalsProvider } from '@grafana/ui';
|
||||||
|
import { GrafanaApp } from './app';
|
||||||
|
import { getAppRoutes } from 'app/routes/routes';
|
||||||
|
import { ConfigContext, ThemeProvider } from './core/utils/ConfigProvider';
|
||||||
|
import { RouteDescriptor } from './core/navigation/types';
|
||||||
|
import { contextSrv } from './core/services/context_srv';
|
||||||
|
import { SideMenu } from './core/components/sidemenu/SideMenu';
|
||||||
|
import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
||||||
|
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||||
|
import { SearchWrapper } from 'app/features/search';
|
||||||
|
|
||||||
|
interface AppWrapperProps {
|
||||||
|
app: GrafanaApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppWrapperState {
|
||||||
|
ngInjector: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState> {
|
||||||
|
container = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
constructor(props: AppWrapperProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
ngInjector: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.container) {
|
||||||
|
this.bootstrapNgApp();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to boot angular app, no container to attach to');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapNgApp() {
|
||||||
|
const injector = this.props.app.angularApp.bootstrap();
|
||||||
|
this.setState({ ngInjector: injector });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRoute = (route: RouteDescriptor) => {
|
||||||
|
const roles = route.roles ? route.roles() : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={route.path}
|
||||||
|
key={route.path}
|
||||||
|
render={(props) => {
|
||||||
|
navigationLogger('AppWrapper', false, 'Rendering route', route, 'with match', props.location);
|
||||||
|
// TODO[Router]: test this logic
|
||||||
|
if (roles?.length) {
|
||||||
|
if (!roles.some((r: string) => contextSrv.hasRole(r))) {
|
||||||
|
return <Redirect to="/" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <GrafanaRoute {...props} route={route} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderRoutes() {
|
||||||
|
return <Switch>{getAppRoutes().map((r) => this.renderRoute(r))}</Switch>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
navigationLogger('AppWrapper', false, 'rendering');
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const appSeed = `<grafana-app ng-cloak></app-notifications-list><div id="ngRoot"></div></grafana-app>`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ErrorBoundaryAlert style="page">
|
||||||
|
<ConfigContext.Provider value={config}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<ModalsProvider>
|
||||||
|
<div className="grafana-app">
|
||||||
|
<Router history={locationService.getHistory()}>
|
||||||
|
<SideMenu />
|
||||||
|
<div className="main-view">
|
||||||
|
<div
|
||||||
|
ref={this.container}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: appSeed,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppNotificationList />
|
||||||
|
<SearchWrapper />
|
||||||
|
{this.state.ngInjector && this.container && this.renderRoutes()}
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
<ModalRoot />
|
||||||
|
</ModalsProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
</ErrorBoundaryAlert>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
118
public/app/angular/AngularApp.ts
Normal file
118
public/app/angular/AngularApp.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import angular from 'angular';
|
||||||
|
import 'angular-route';
|
||||||
|
import 'angular-sanitize';
|
||||||
|
import 'angular-bindonce';
|
||||||
|
import 'vendor/bootstrap/bootstrap';
|
||||||
|
import 'vendor/angular-other/angular-strap';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { angularModules } from 'app/core/core_module';
|
||||||
|
import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
|
import { registerAngularDirectives } from 'app/core/core';
|
||||||
|
import { initAngularRoutingBridge } from 'app/angular/bridgeReactAngularRouting';
|
||||||
|
import { monkeyPatchInjectorWithPreAssignedBindings } from 'app/core/injectorMonkeyPatch';
|
||||||
|
import { extend } from 'lodash';
|
||||||
|
|
||||||
|
export class AngularApp {
|
||||||
|
ngModuleDependencies: any[];
|
||||||
|
preBootModules: any[];
|
||||||
|
registerFunctions: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.preBootModules = [];
|
||||||
|
this.ngModuleDependencies = [];
|
||||||
|
this.registerFunctions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const app = angular.module('grafana', []);
|
||||||
|
|
||||||
|
app.config(
|
||||||
|
(
|
||||||
|
$controllerProvider: angular.IControllerProvider,
|
||||||
|
$compileProvider: angular.ICompileProvider,
|
||||||
|
$filterProvider: angular.IFilterProvider,
|
||||||
|
$httpProvider: angular.IHttpProvider,
|
||||||
|
$provide: angular.auto.IProvideService
|
||||||
|
) => {
|
||||||
|
if (config.buildInfo.env !== 'development') {
|
||||||
|
$compileProvider.debugInfoEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$httpProvider.useApplyAsync(true);
|
||||||
|
|
||||||
|
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',
|
||||||
|
'$strap.directives',
|
||||||
|
'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);
|
||||||
|
|
||||||
|
registerAngularDirectives();
|
||||||
|
initAngularRoutingBridge();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, this.ngModuleDependencies);
|
||||||
|
|
||||||
|
monkeyPatchInjectorWithPreAssignedBindings(injector);
|
||||||
|
|
||||||
|
console.log('Angular app bootstrap');
|
||||||
|
|
||||||
|
injector.invoke(() => {
|
||||||
|
this.preBootModules.forEach((module) => {
|
||||||
|
extend(module, this.registerFunctions);
|
||||||
|
});
|
||||||
|
|
||||||
|
// I don't know
|
||||||
|
return () => {};
|
||||||
|
});
|
||||||
|
|
||||||
|
return injector;
|
||||||
|
}
|
||||||
|
}
|
201
public/app/angular/AngularLocationWrapper.test.ts
Normal file
201
public/app/angular/AngularLocationWrapper.test.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { AngularLocationWrapper } from './AngularLocationWrapper';
|
||||||
|
import { HistoryWrapper, locationService, setLocationService } from '@grafana/runtime';
|
||||||
|
|
||||||
|
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, any> = { 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('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
127
public/app/angular/AngularLocationWrapper.ts
Normal file
127
public/app/angular/AngularLocationWrapper.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { locationSearchToObject, locationService, navigationLogger } from '@grafana/runtime';
|
||||||
|
import { urlUtil } from '@grafana/data';
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
absUrl(): string {
|
||||||
|
return `${window.location.origin}${this.url()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash(newHash?: any) {
|
||||||
|
navigationLogger('AngularLocationWrapper', false, 'Angular compat layer: hash');
|
||||||
|
if (!newHash) {
|
||||||
|
return locationService.getLocation().hash.substr(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 of Object.keys(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) {
|
||||||
|
console.log('pushing emptu');
|
||||||
|
locationService.push('/');
|
||||||
|
} else {
|
||||||
|
locationService.push(newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = locationService.getLocation();
|
||||||
|
return `${location.pathname}${location.search}${location.hash}`;
|
||||||
|
}
|
||||||
|
}
|
47
public/app/angular/bridgeReactAngularRouting.ts
Normal file
47
public/app/angular/bridgeReactAngularRouting.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { coreModule } from '../core/core_module';
|
||||||
|
import { RouteProvider } from '../core/navigation/patch/RouteProvider';
|
||||||
|
import { RouteParamsProvider } from '../core/navigation/patch/RouteParamsProvider';
|
||||||
|
import { ILocationService } from 'angular';
|
||||||
|
import { AngularLocationWrapper } from './AngularLocationWrapper';
|
||||||
|
|
||||||
|
// 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 ILocationService;
|
||||||
|
return $delegate;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
coreModule.provider('$route', RouteProvider);
|
||||||
|
coreModule.provider('$routeParams', RouteParamsProvider);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initAngularRoutingBridge() {
|
||||||
|
tamperAngularLocation();
|
||||||
|
interceptAngularLocation();
|
||||||
|
}
|
@ -10,32 +10,19 @@ import ttiPolyfill from 'tti-polyfill';
|
|||||||
import 'file-saver';
|
import 'file-saver';
|
||||||
import 'jquery';
|
import 'jquery';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import angular from 'angular';
|
import ReactDOM from 'react-dom';
|
||||||
import 'angular-route';
|
import React from 'react';
|
||||||
import 'angular-sanitize';
|
|
||||||
import 'angular-bindonce';
|
|
||||||
import 'react';
|
|
||||||
import 'react-dom';
|
|
||||||
|
|
||||||
import 'vendor/bootstrap/bootstrap';
|
|
||||||
import 'vendor/angular-other/angular-strap';
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
|
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
|
||||||
import {
|
import {
|
||||||
AppEvents,
|
|
||||||
setLocale,
|
setLocale,
|
||||||
setTimeZoneResolver,
|
setTimeZoneResolver,
|
||||||
standardEditorsRegistry,
|
standardEditorsRegistry,
|
||||||
standardFieldConfigEditorRegistry,
|
standardFieldConfigEditorRegistry,
|
||||||
standardTransformersRegistry,
|
standardTransformersRegistry,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { checkBrowserCompatibility } from 'app/core/utils/browser';
|
|
||||||
import { arrayMove } from 'app/core/utils/arrayMove';
|
import { arrayMove } from 'app/core/utils/arrayMove';
|
||||||
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||||
import { angularModules, coreModule } from 'app/core/core_module';
|
|
||||||
import { registerAngularDirectives } from 'app/core/core';
|
|
||||||
import { setupAngularRoutes } from 'app/routes/routes';
|
|
||||||
import { registerEchoBackend, setEchoSrv } from '@grafana/runtime';
|
import { registerEchoBackend, setEchoSrv } from '@grafana/runtime';
|
||||||
import { Echo } from './core/services/echo/Echo';
|
import { Echo } from './core/services/echo/Echo';
|
||||||
import { reportPerformance } from './core/services/echo/EchoSrv';
|
import { reportPerformance } from './core/services/echo/EchoSrv';
|
||||||
@ -47,8 +34,11 @@ import { getDefaultVariableAdapters, variableAdapters } from './features/variabl
|
|||||||
import { initDevFeatures } from './dev';
|
import { initDevFeatures } from './dev';
|
||||||
import { getStandardTransformers } from 'app/core/utils/standardTransformers';
|
import { getStandardTransformers } from 'app/core/utils/standardTransformers';
|
||||||
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
|
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
|
||||||
import { monkeyPatchInjectorWithPreAssignedBindings } from './core/injectorMonkeyPatch';
|
|
||||||
import { setVariableQueryRunner, VariableQueryRunner } from './features/variables/query/VariableQueryRunner';
|
import { setVariableQueryRunner, VariableQueryRunner } from './features/variables/query/VariableQueryRunner';
|
||||||
|
import { configureStore } from './store/configureStore';
|
||||||
|
import { AppWrapper } from './AppWrapper';
|
||||||
|
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||||
|
import { AngularApp } from './angular/AngularApp';
|
||||||
|
|
||||||
// add move to lodash for backward compatabilty with plugins
|
// add move to lodash for backward compatabilty with plugins
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -56,8 +46,8 @@ _.move = arrayMove;
|
|||||||
|
|
||||||
// import symlinked extensions
|
// import symlinked extensions
|
||||||
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
||||||
extensionsIndex.keys().forEach((key: any) => {
|
const extensionsExports = extensionsIndex.keys().map((key: any) => {
|
||||||
extensionsIndex(key);
|
return extensionsIndex(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
@ -65,32 +55,20 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GrafanaApp {
|
export class GrafanaApp {
|
||||||
registerFunctions: any;
|
angularApp: AngularApp;
|
||||||
ngModuleDependencies: any[];
|
|
||||||
preBootModules: any[] | null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.preBootModules = [];
|
this.angularApp = new AngularApp();
|
||||||
this.registerFunctions = {};
|
|
||||||
this.ngModuleDependencies = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
useModule(module: angular.IModule) {
|
|
||||||
if (this.preBootModules) {
|
|
||||||
this.preBootModules.push(module);
|
|
||||||
} else {
|
|
||||||
_.extend(module, this.registerFunctions);
|
|
||||||
}
|
|
||||||
this.ngModuleDependencies.push(module.name);
|
|
||||||
return module;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
const app = angular.module('grafana', []);
|
initEchoSrv();
|
||||||
|
|
||||||
addClassIfNoOverlayScrollbar();
|
addClassIfNoOverlayScrollbar();
|
||||||
setLocale(config.bootData.user.locale);
|
setLocale(config.bootData.user.locale);
|
||||||
setTimeZoneResolver(() => config.bootData.user.timezone);
|
setTimeZoneResolver(() => config.bootData.user.timezone);
|
||||||
|
// Important that extensions are initialized before store
|
||||||
|
initExtensions();
|
||||||
|
configureStore();
|
||||||
|
|
||||||
standardEditorsRegistry.setInit(getStandardOptionEditors);
|
standardEditorsRegistry.setInit(getStandardOptionEditors);
|
||||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||||
@ -99,97 +77,38 @@ export class GrafanaApp {
|
|||||||
|
|
||||||
setVariableQueryRunner(new VariableQueryRunner());
|
setVariableQueryRunner(new VariableQueryRunner());
|
||||||
|
|
||||||
app.config(
|
// intercept anchor clicks and forward it to custom history instead of relying on browser's history
|
||||||
(
|
document.addEventListener('click', interceptLinkClicks);
|
||||||
$controllerProvider: angular.IControllerProvider,
|
|
||||||
$compileProvider: angular.ICompileProvider,
|
|
||||||
$filterProvider: angular.IFilterProvider,
|
|
||||||
$httpProvider: angular.IHttpProvider,
|
|
||||||
$provide: angular.auto.IProvideService
|
|
||||||
) => {
|
|
||||||
if (config.buildInfo.env !== 'development') {
|
|
||||||
$compileProvider.debugInfoEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$httpProvider.useApplyAsync(true);
|
|
||||||
|
|
||||||
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',
|
|
||||||
'ngRoute',
|
|
||||||
'ngSanitize',
|
|
||||||
'$strap.directives',
|
|
||||||
'grafana',
|
|
||||||
'pasvaz.bindonce',
|
|
||||||
'react',
|
|
||||||
];
|
|
||||||
|
|
||||||
// makes it possible to add dynamic stuff
|
|
||||||
_.each(angularModules, (m: angular.IModule) => {
|
|
||||||
this.useModule(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
// register react angular wrappers
|
|
||||||
coreModule.config(setupAngularRoutes);
|
|
||||||
registerAngularDirectives();
|
|
||||||
|
|
||||||
// disable tool tip animation
|
// disable tool tip animation
|
||||||
$.fn.tooltip.defaults.animation = false;
|
$.fn.tooltip.defaults.animation = false;
|
||||||
|
|
||||||
// bootstrap the app
|
this.angularApp.init();
|
||||||
const injector: any = angular.bootstrap(document, this.ngModuleDependencies);
|
|
||||||
|
|
||||||
injector.invoke(() => {
|
|
||||||
_.each(this.preBootModules, (module: angular.IModule) => {
|
|
||||||
_.extend(module, this.registerFunctions);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.preBootModules = null;
|
|
||||||
|
|
||||||
if (!checkBrowserCompatibility()) {
|
|
||||||
setTimeout(() => {
|
|
||||||
appEvents.emit(AppEvents.alertWarning, [
|
|
||||||
'Your browser is not fully supported',
|
|
||||||
'A newer browser version is recommended',
|
|
||||||
]);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
monkeyPatchInjectorWithPreAssignedBindings(injector);
|
|
||||||
|
|
||||||
// Preload selected app plugins
|
// Preload selected app plugins
|
||||||
|
const promises = [];
|
||||||
for (const modulePath of config.pluginsToPreload) {
|
for (const modulePath of config.pluginsToPreload) {
|
||||||
importPluginModule(modulePath);
|
promises.push(importPluginModule(modulePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
|
ReactDOM.render(
|
||||||
|
React.createElement(AppWrapper, {
|
||||||
|
app: this,
|
||||||
|
}),
|
||||||
|
document.getElementById('reactRoot')
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initEchoSrv() {
|
function initExtensions() {
|
||||||
|
if (extensionsExports.length > 0) {
|
||||||
|
extensionsExports[0].init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEchoSrv() {
|
||||||
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
||||||
|
|
||||||
ttiPolyfill.getFirstConsistentlyInteractive().then((tti: any) => {
|
ttiPolyfill.getFirstConsistentlyInteractive().then((tti: any) => {
|
||||||
@ -217,7 +136,6 @@ export class GrafanaApp {
|
|||||||
reportPerformance('dcl', Math.round(performance.now()));
|
reportPerformance('dcl', Math.round(performance.now()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function addClassIfNoOverlayScrollbar() {
|
function addClassIfNoOverlayScrollbar() {
|
||||||
if (getScrollbarWidth() > 0) {
|
if (getScrollbarWidth() > 0) {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { clearAppNotification, notifyApp } from '../reducers/appNotification';
|
import { clearAppNotification, notifyApp } from '../reducers/appNotification';
|
||||||
import { updateLocation } from '../reducers/location';
|
|
||||||
import { updateNavIndex, updateConfigurationSubtitle } from '../reducers/navModel';
|
import { updateNavIndex, updateConfigurationSubtitle } from '../reducers/navModel';
|
||||||
|
export { updateNavIndex, updateConfigurationSubtitle, notifyApp, clearAppNotification };
|
||||||
export { updateLocation, updateNavIndex, updateConfigurationSubtitle, notifyApp, clearAppNotification };
|
|
||||||
|
@ -5,9 +5,7 @@ import { AnnotationQueryEditor as CloudWatchAnnotationQueryEditor } from 'app/pl
|
|||||||
import PageHeader from './components/PageHeader/PageHeader';
|
import PageHeader from './components/PageHeader/PageHeader';
|
||||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
|
||||||
import { MetricSelect } from './components/Select/MetricSelect';
|
import { MetricSelect } from './components/Select/MetricSelect';
|
||||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
|
||||||
import {
|
import {
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
DataLinksInlineEditor,
|
DataLinksInlineEditor,
|
||||||
@ -24,7 +22,7 @@ import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/component
|
|||||||
import { HelpModal } from './components/help/HelpModal';
|
import { HelpModal } from './components/help/HelpModal';
|
||||||
import { Footer } from './components/Footer/Footer';
|
import { Footer } from './components/Footer/Footer';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
import { SearchField, SearchResults, SearchResultsFilter, SearchWrapper } from '../features/search';
|
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
|
||||||
|
|
||||||
const { SecretFormField } = LegacyForms;
|
const { SecretFormField } = LegacyForms;
|
||||||
|
|
||||||
@ -38,9 +36,7 @@ export function registerAngularDirectives() {
|
|||||||
]);
|
]);
|
||||||
react2AngularDirective('spinner', Spinner, ['inline']);
|
react2AngularDirective('spinner', Spinner, ['inline']);
|
||||||
react2AngularDirective('helpModal', HelpModal, []);
|
react2AngularDirective('helpModal', HelpModal, []);
|
||||||
react2AngularDirective('sidemenu', SideMenu, []);
|
|
||||||
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
|
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
|
||||||
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||||
react2AngularDirective('emptyListCta', EmptyListCTA, [
|
react2AngularDirective('emptyListCta', EmptyListCTA, [
|
||||||
'title',
|
'title',
|
||||||
@ -84,7 +80,6 @@ export function registerAngularDirectives() {
|
|||||||
['onStarredFilterChange', { watchDepth: 'reference' }],
|
['onStarredFilterChange', { watchDepth: 'reference' }],
|
||||||
['onTagFilterChange', { watchDepth: 'reference' }],
|
['onTagFilterChange', { watchDepth: 'reference' }],
|
||||||
]);
|
]);
|
||||||
react2AngularDirective('searchWrapper', SearchWrapper, []);
|
|
||||||
react2AngularDirective('tagFilter', TagFilter, [
|
react2AngularDirective('tagFilter', TagFilter, [
|
||||||
'tags',
|
'tags',
|
||||||
['onChange', { watchDepth: 'reference' }],
|
['onChange', { watchDepth: 'reference' }],
|
||||||
|
@ -2,8 +2,7 @@ import React, { PureComponent } from 'react';
|
|||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import AppNotificationItem from './AppNotificationItem';
|
import AppNotificationItem from './AppNotificationItem';
|
||||||
import { notifyApp, clearAppNotification } from 'app/core/actions';
|
import { notifyApp, clearAppNotification } from 'app/core/actions';
|
||||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
import { StoreState } from 'app/types';
|
||||||
import { AppNotification, StoreState } from 'app/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createErrorNotification,
|
createErrorNotification,
|
||||||
@ -11,14 +10,24 @@ import {
|
|||||||
createWarningNotification,
|
createWarningNotification,
|
||||||
} from '../../copy/appNotification';
|
} from '../../copy/appNotification';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
export interface Props {
|
export interface OwnProps {}
|
||||||
appNotifications: AppNotification[];
|
|
||||||
notifyApp: typeof notifyApp;
|
|
||||||
clearAppNotification: typeof clearAppNotification;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AppNotificationList extends PureComponent<Props> {
|
const mapStateToProps = (state: StoreState, props: OwnProps) => ({
|
||||||
|
appNotifications: state.appNotifications.appNotifications,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
notifyApp,
|
||||||
|
clearAppNotification,
|
||||||
|
};
|
||||||
|
|
||||||
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
export class AppNotificationListUnConnected extends PureComponent<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { notifyApp } = this.props;
|
const { notifyApp } = this.props;
|
||||||
|
|
||||||
@ -35,7 +44,7 @@ export class AppNotificationList extends PureComponent<Props> {
|
|||||||
const { appNotifications } = this.props;
|
const { appNotifications } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="page-alert-list">
|
||||||
{appNotifications.map((appNotification, index) => {
|
{appNotifications.map((appNotification, index) => {
|
||||||
return (
|
return (
|
||||||
<AppNotificationItem
|
<AppNotificationItem
|
||||||
@ -50,13 +59,4 @@ export class AppNotificationList extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
export const AppNotificationList = connector(AppNotificationListUnConnected);
|
||||||
appNotifications: state.appNotifications.appNotifications,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
notifyApp,
|
|
||||||
clearAppNotification,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps);
|
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import Loadable from 'react-loadable';
|
import Loadable from 'react-loadable';
|
||||||
import { LoadingChunkPlaceHolder } from './LoadingChunkPlaceHolder';
|
import { LoadingChunkPlaceHolder } from './LoadingChunkPlaceHolder';
|
||||||
import { ErrorLoadingChunk } from './ErrorLoadingChunk';
|
import { ErrorLoadingChunk } from './ErrorLoadingChunk';
|
||||||
|
import { GrafanaRouteComponent } from 'app/core/navigation/types';
|
||||||
|
|
||||||
export const loadComponentHandler = (props: { error: Error; pastDelay: boolean }) => {
|
export const loadComponentHandler = (props: { error: Error; pastDelay: boolean }) => {
|
||||||
const { error, pastDelay } = props;
|
const { error, pastDelay } = props;
|
||||||
@ -17,11 +18,8 @@ export const loadComponentHandler = (props: { error: Error; pastDelay: boolean }
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SafeDynamicImport = (importStatement: Promise<any>) => ({ ...props }) => {
|
export const SafeDynamicImport = (loader: () => Promise<any>): GrafanaRouteComponent =>
|
||||||
const LoadableComponent = Loadable({
|
Loadable({
|
||||||
loader: () => importStatement,
|
loader: loader,
|
||||||
loading: loadComponentHandler,
|
loading: loadComponentHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <LoadableComponent {...props} />;
|
|
||||||
};
|
|
||||||
|
@ -2,12 +2,17 @@ import React, { FC } from 'react';
|
|||||||
import { LoginLayout, InnerBox } from '../Login/LoginLayout';
|
import { LoginLayout, InnerBox } from '../Login/LoginLayout';
|
||||||
import { ChangePassword } from './ChangePassword';
|
import { ChangePassword } from './ChangePassword';
|
||||||
import LoginCtrl from '../Login/LoginCtrl';
|
import LoginCtrl from '../Login/LoginCtrl';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
export const ChangePasswordPage: FC = () => {
|
interface Props extends GrafanaRouteComponentProps<{}, { code: string }> {}
|
||||||
|
|
||||||
|
export const ChangePasswordPage: FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<LoginLayout>
|
<LoginLayout>
|
||||||
<InnerBox>
|
<InnerBox>
|
||||||
<LoginCtrl>{({ changePassword }) => <ChangePassword onSubmit={changePassword} />}</LoginCtrl>
|
<LoginCtrl resetCode={props.queryParams.code}>
|
||||||
|
{({ changePassword }) => <ChangePassword onSubmit={changePassword} />}
|
||||||
|
</LoginCtrl>
|
||||||
</InnerBox>
|
</InnerBox>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { StoreState } from 'app/types';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { hot } from 'react-hot-loader';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
|
|
||||||
@ -18,9 +13,10 @@ export interface FormModel {
|
|||||||
password: string;
|
password: string;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
routeParams?: any;
|
resetCode?: string;
|
||||||
updateLocation?: typeof updateLocation;
|
|
||||||
children: (props: {
|
children: (props: {
|
||||||
isLoggingIn: boolean;
|
isLoggingIn: boolean;
|
||||||
changePassword: (pw: string) => void;
|
changePassword: (pw: string) => void;
|
||||||
@ -44,6 +40,7 @@ interface State {
|
|||||||
|
|
||||||
export class LoginCtrl extends PureComponent<Props, State> {
|
export class LoginCtrl extends PureComponent<Props, State> {
|
||||||
result: any = {};
|
result: any = {};
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -62,7 +59,8 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
|||||||
confirmNew: password,
|
confirmNew: password,
|
||||||
oldPassword: 'admin',
|
oldPassword: 'admin',
|
||||||
};
|
};
|
||||||
if (!this.props.routeParams.code) {
|
|
||||||
|
if (!this.props.resetCode) {
|
||||||
getBackendSrv()
|
getBackendSrv()
|
||||||
.put('/api/user/password', pw)
|
.put('/api/user/password', pw)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -72,7 +70,7 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resetModel = {
|
const resetModel = {
|
||||||
code: this.props.routeParams.code,
|
code: this.props.resetCode,
|
||||||
newPassword: password,
|
newPassword: password,
|
||||||
confirmPassword: password,
|
confirmPassword: password,
|
||||||
};
|
};
|
||||||
@ -153,10 +151,4 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapStateToProps = (state: StoreState) => ({
|
export default LoginCtrl;
|
||||||
routeParams: state.location.routeParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = { updateLocation };
|
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LoginCtrl));
|
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
import React, { FC } from 'react';
|
|
||||||
import { connect, MapStateToProps } from 'react-redux';
|
|
||||||
import { StoreState } from 'app/types';
|
|
||||||
import { Form, Field, Input, Button, HorizontalGroup, LinkButton } from '@grafana/ui';
|
|
||||||
import { getConfig } from 'app/core/config';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { AppEvents } from '@grafana/data';
|
|
||||||
|
|
||||||
interface SignupDTO {
|
|
||||||
name?: string;
|
|
||||||
email: string;
|
|
||||||
username: string;
|
|
||||||
orgName?: string;
|
|
||||||
password: string;
|
|
||||||
code: string;
|
|
||||||
confirm?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConnectedProps {
|
|
||||||
email?: string;
|
|
||||||
code?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SignupUnconnected: FC<ConnectedProps> = (props) => {
|
|
||||||
const onSubmit = async (formData: SignupDTO) => {
|
|
||||||
if (formData.name === '') {
|
|
||||||
delete formData.name;
|
|
||||||
}
|
|
||||||
delete formData.confirm;
|
|
||||||
|
|
||||||
const response = await getBackendSrv()
|
|
||||||
.post('/api/user/signup/step2', {
|
|
||||||
email: formData.email,
|
|
||||||
code: formData.code,
|
|
||||||
username: formData.email,
|
|
||||||
orgName: formData.orgName,
|
|
||||||
password: formData.password,
|
|
||||||
name: formData.name,
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const msg = err.data?.message || err;
|
|
||||||
appEvents.emit(AppEvents.alertWarning, [msg]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.code === 'redirect-to-select-org') {
|
|
||||||
window.location.href = getConfig().appSubUrl + '/profile/select-org?signup=1';
|
|
||||||
}
|
|
||||||
window.location.href = getConfig().appSubUrl + '/';
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
email: props.email,
|
|
||||||
code: props.code,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form defaultValues={defaultValues} onSubmit={onSubmit}>
|
|
||||||
{({ errors, register, getValues }) => (
|
|
||||||
<>
|
|
||||||
<Field label="Your name">
|
|
||||||
<Input name="name" placeholder="(optional)" ref={register} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Email" invalid={!!errors.email} error={errors.email?.message}>
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Email"
|
|
||||||
ref={register({
|
|
||||||
required: 'Email is required',
|
|
||||||
pattern: {
|
|
||||||
value: /^\S+@\S+$/,
|
|
||||||
message: 'Email is invalid',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{!getConfig().autoAssignOrg && (
|
|
||||||
<Field label="Org. name">
|
|
||||||
<Input name="orgName" placeholder="Org. name" ref={register} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
{getConfig().verifyEmailEnabled && (
|
|
||||||
<Field label="Email verification code (sent to your email)">
|
|
||||||
<Input name="code" ref={register} placeholder="Code" />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
ref={register({
|
|
||||||
required: 'Password is required',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
name="confirm"
|
|
||||||
ref={register({
|
|
||||||
required: 'Confirmed password is required',
|
|
||||||
validate: (v) => v === getValues().password || 'Passwords must match!',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Button type="submit">Submit</Button>
|
|
||||||
<LinkButton variant="link" href={getConfig().appSubUrl + '/login'}>
|
|
||||||
Back to login
|
|
||||||
</LinkButton>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps: MapStateToProps<ConnectedProps, {}, StoreState> = (state: StoreState) => ({
|
|
||||||
email: state.location.routeParams.email?.toString(),
|
|
||||||
code: state.location.routeParams.code?.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Signup = connect(mapStateToProps)(SignupUnconnected);
|
|
@ -1,12 +1,124 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { LoginLayout, InnerBox } from '../Login/LoginLayout';
|
import { Form, Field, Input, Button, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||||
import { Signup } from './Signup';
|
import { getConfig } from 'app/core/config';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { AppEvents } from '@grafana/data';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
import { InnerBox, LoginLayout } from '../Login/LoginLayout';
|
||||||
|
|
||||||
|
interface SignupDTO {
|
||||||
|
name?: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
orgName?: string;
|
||||||
|
password: string;
|
||||||
|
code: string;
|
||||||
|
confirm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryParams {
|
||||||
|
email?: string;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends GrafanaRouteComponentProps<{}, QueryParams> {}
|
||||||
|
|
||||||
|
export const SignupPage: FC<Props> = (props) => {
|
||||||
|
const onSubmit = async (formData: SignupDTO) => {
|
||||||
|
if (formData.name === '') {
|
||||||
|
delete formData.name;
|
||||||
|
}
|
||||||
|
delete formData.confirm;
|
||||||
|
|
||||||
|
const response = await getBackendSrv()
|
||||||
|
.post('/api/user/signup/step2', {
|
||||||
|
email: formData.email,
|
||||||
|
code: formData.code,
|
||||||
|
username: formData.email,
|
||||||
|
orgName: formData.orgName,
|
||||||
|
password: formData.password,
|
||||||
|
name: formData.name,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const msg = err.data?.message || err;
|
||||||
|
appEvents.emit(AppEvents.alertWarning, [msg]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 'redirect-to-select-org') {
|
||||||
|
window.location.href = getConfig().appSubUrl + '/profile/select-org?signup=1';
|
||||||
|
}
|
||||||
|
window.location.href = getConfig().appSubUrl + '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
email: props.queryParams.email,
|
||||||
|
code: props.queryParams.code,
|
||||||
|
};
|
||||||
|
|
||||||
export const SignupPage: FC = () => {
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout>
|
<LoginLayout>
|
||||||
<InnerBox>
|
<InnerBox>
|
||||||
<Signup />
|
<Form defaultValues={defaultValues} onSubmit={onSubmit}>
|
||||||
|
{({ errors, register, getValues }) => (
|
||||||
|
<>
|
||||||
|
<Field label="Your name">
|
||||||
|
<Input name="name" placeholder="(optional)" ref={register} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Email" invalid={!!errors.email} error={errors.email?.message}>
|
||||||
|
<Input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
ref={register({
|
||||||
|
required: 'Email is required',
|
||||||
|
pattern: {
|
||||||
|
value: /^\S+@\S+$/,
|
||||||
|
message: 'Email is invalid',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
{!getConfig().autoAssignOrg && (
|
||||||
|
<Field label="Org. name">
|
||||||
|
<Input name="orgName" placeholder="Org. name" ref={register} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
{getConfig().verifyEmailEnabled && (
|
||||||
|
<Field label="Email verification code (sent to your email)">
|
||||||
|
<Input name="code" ref={register} placeholder="Code" />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
ref={register({
|
||||||
|
required: 'Password is required',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="confirm"
|
||||||
|
ref={register({
|
||||||
|
required: 'Confirmed password is required',
|
||||||
|
validate: (v) => v === getValues().password || 'Passwords must match!',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<HorizontalGroup>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
<LinkButton variant="link" href={getConfig().appSubUrl + '/login'}>
|
||||||
|
Back to login
|
||||||
|
</LinkButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
</InnerBox>
|
</InnerBox>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
);
|
);
|
||||||
|
@ -169,7 +169,7 @@ export class FormDropdownCtrl {
|
|||||||
this.linkMode = true;
|
this.linkMode = true;
|
||||||
this.inputElement.hide();
|
this.inputElement.hide();
|
||||||
this.linkElement.show();
|
this.linkElement.show();
|
||||||
this.updateValue(this.inputElement.val());
|
this.updateValue(this.inputElement.val() as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputBlur() {
|
inputBlur() {
|
||||||
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import BottomNavLinks from './BottomNavLinks';
|
import BottomNavLinks from './BottomNavLinks';
|
||||||
import appEvents from '../../app_events';
|
import appEvents from '../../app_events';
|
||||||
import { CoreEvents } from 'app/types';
|
import { ShowModalEvent } from '../../../types/events';
|
||||||
|
|
||||||
jest.mock('../../app_events', () => ({
|
jest.mock('../../app_events', () => ({
|
||||||
emit: jest.fn(),
|
publish: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
@ -94,7 +94,11 @@ describe('Functions', () => {
|
|||||||
const instance = wrapper.instance() as BottomNavLinks;
|
const instance = wrapper.instance() as BottomNavLinks;
|
||||||
instance.onOpenShortcuts();
|
instance.onOpenShortcuts();
|
||||||
|
|
||||||
expect(appEvents.emit).toHaveBeenCalledWith(CoreEvents.showModal, { templateHtml: '<help-modal></help-modal>' });
|
expect(appEvents.publish).toHaveBeenCalledWith(
|
||||||
|
new ShowModalEvent({
|
||||||
|
templateHtml: '<help-modal></help-modal>',
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,10 @@ import { css } from 'emotion';
|
|||||||
import appEvents from '../../app_events';
|
import appEvents from '../../app_events';
|
||||||
import { User } from '../../services/context_srv';
|
import { User } from '../../services/context_srv';
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { Icon, IconName } from '@grafana/ui';
|
import { Icon, IconName, Link } from '@grafana/ui';
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
import { OrgSwitcher } from '../OrgSwitcher';
|
import { OrgSwitcher } from '../OrgSwitcher';
|
||||||
import { getFooterLinks } from '../Footer/Footer';
|
import { getFooterLinks } from '../Footer/Footer';
|
||||||
|
import { ShowModalEvent } from '../../../types/events';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
link: NavModelItem;
|
link: NavModelItem;
|
||||||
@ -23,9 +23,11 @@ export default class BottomNavLinks extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onOpenShortcuts = () => {
|
onOpenShortcuts = () => {
|
||||||
appEvents.emit(CoreEvents.showModal, {
|
appEvents.publish(
|
||||||
|
new ShowModalEvent({
|
||||||
templateHtml: '<help-modal></help-modal>',
|
templateHtml: '<help-modal></help-modal>',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleSwitcherModal = () => {
|
toggleSwitcherModal = () => {
|
||||||
@ -49,12 +51,12 @@ export default class BottomNavLinks extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidemenu-item dropdown dropup">
|
<div className="sidemenu-item dropdown dropup">
|
||||||
<a href={link.url} className="sidemenu-link" target={link.target}>
|
<Link href={link.url} className="sidemenu-link" target={link.target}>
|
||||||
<span className="icon-circle sidemenu-icon">
|
<span className="icon-circle sidemenu-icon">
|
||||||
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
{link.icon && <Icon name={link.icon as IconName} size="xl" />}
|
||||||
{link.img && <img src={link.img} />}
|
{link.img && <img src={link.img} />}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</Link>
|
||||||
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
{link.subTitle && (
|
{link.subTitle && (
|
||||||
<li className="sidemenu-subtitle">
|
<li className="sidemenu-subtitle">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import SignIn from './SignIn';
|
import { SignIn } from './SignIn';
|
||||||
import BottomNavLinks from './BottomNavLinks';
|
import BottomNavLinks from './BottomNavLinks';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Icon, IconName, useTheme } from '@grafana/ui';
|
import { Icon, IconName, Link, useTheme } from '@grafana/ui';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
child: any;
|
child: any;
|
||||||
@ -14,14 +14,16 @@ const DropDownChild: FC<Props> = (props) => {
|
|||||||
margin-right: ${theme.spacing.sm};
|
margin-right: ${theme.spacing.sm};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
const linkContent = (
|
||||||
<li className={listItemClassName}>
|
<>
|
||||||
<a href={child.url}>
|
|
||||||
{child.icon && <Icon name={child.icon as IconName} className={iconClassName} />}
|
{child.icon && <Icon name={child.icon as IconName} className={iconClassName} />}
|
||||||
{child.text}
|
{child.text}
|
||||||
</a>
|
</>
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const anchor = child.url ? <Link href={child.url}>{linkContent}</Link> : <a>{linkContent}</a>;
|
||||||
|
|
||||||
|
return <li className={listItemClassName}>{anchor}</li>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DropDownChild;
|
export default DropDownChild;
|
||||||
|
@ -1,22 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { SideMenu } from './SideMenu';
|
import { SideMenu } from './SideMenu';
|
||||||
import appEvents from '../../app_events';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { CoreEvents } from 'app/types';
|
import { Router } from 'react-router-dom';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
jest.mock('../../app_events', () => ({
|
import { configureStore } from 'app/store/configureStore';
|
||||||
emit: jest.fn(),
|
import { Provider } from 'react-redux';
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('app/store/store', () => ({
|
|
||||||
store: {
|
|
||||||
getState: jest.fn().mockReturnValue({
|
|
||||||
location: {
|
|
||||||
lastUpdated: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
contextSrv: {
|
contextSrv: {
|
||||||
@ -29,37 +17,30 @@ jest.mock('app/core/services/context_srv', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = () => {
|
||||||
const props = Object.assign(
|
const store = configureStore();
|
||||||
{
|
|
||||||
loginUrl: '',
|
|
||||||
user: {},
|
|
||||||
mainLinks: [],
|
|
||||||
bottomeLinks: [],
|
|
||||||
isSignedIn: false,
|
|
||||||
},
|
|
||||||
propOverrides
|
|
||||||
);
|
|
||||||
|
|
||||||
return shallow(<SideMenu {...props} />);
|
return render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router history={locationService.getHistory()}>
|
||||||
|
<SideMenu />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
it('should render component', () => {
|
it('should render component', async () => {
|
||||||
const wrapper = setup();
|
setup();
|
||||||
|
const sidemenu = await screen.findByTestId('sidemenu');
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(sidemenu).toBeInTheDocument();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Functions', () => {
|
it('should not render when in kiosk mode', async () => {
|
||||||
describe('toggle side menu on mobile', () => {
|
setup();
|
||||||
const wrapper = setup();
|
|
||||||
const instance = wrapper.instance() as SideMenu;
|
|
||||||
instance.toggleSideMenuSmallBreakpoint();
|
|
||||||
|
|
||||||
it('should emit toggle sidemenu event', () => {
|
locationService.partial({ kiosk: 'full' });
|
||||||
expect(appEvents.emit).toHaveBeenCalledWith(CoreEvents.toggleSidemenuMobile);
|
const sidemenu = screen.queryByTestId('sidemenu');
|
||||||
});
|
expect(sidemenu).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,33 +1,44 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
import appEvents from '../../app_events';
|
import appEvents from '../../app_events';
|
||||||
import TopSection from './TopSection';
|
import TopSection from './TopSection';
|
||||||
import BottomSection from './BottomSection';
|
import BottomSection from './BottomSection';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { CoreEvents } from 'app/types';
|
import { CoreEvents, KioskMode } from 'app/types';
|
||||||
import { Branding } from 'app/core/components/Branding/Branding';
|
import { Branding } from 'app/core/components/Branding/Branding';
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
const homeUrl = config.appSubUrl || '/';
|
const homeUrl = config.appSubUrl || '/';
|
||||||
|
|
||||||
export class SideMenu extends PureComponent {
|
export const SideMenu: FC = React.memo(() => {
|
||||||
toggleSideMenuSmallBreakpoint = () => {
|
const location = useLocation();
|
||||||
appEvents.emit(CoreEvents.toggleSidemenuMobile);
|
const query = new URLSearchParams(location.search);
|
||||||
};
|
const kiosk = query.get('kiosk') as KioskMode;
|
||||||
|
|
||||||
render() {
|
const toggleSideMenuSmallBreakpoint = useCallback(() => {
|
||||||
return [
|
appEvents.emit(CoreEvents.toggleSidemenuMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (kiosk !== null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sidemenu" data-testid="sidemenu">
|
||||||
<a href={homeUrl} className="sidemenu__logo" key="logo">
|
<a href={homeUrl} className="sidemenu__logo" key="logo">
|
||||||
<Branding.MenuLogo />
|
<Branding.MenuLogo />
|
||||||
</a>,
|
</a>
|
||||||
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
<div className="sidemenu__logo_small_breakpoint" onClick={toggleSideMenuSmallBreakpoint} key="hamburger">
|
||||||
<Icon name="bars" size="xl" />
|
<Icon name="bars" size="xl" />
|
||||||
<span className="sidemenu__close">
|
<span className="sidemenu__close">
|
||||||
<Icon name="times" />
|
<Icon name="times" />
|
||||||
Close
|
Close
|
||||||
</span>
|
</span>
|
||||||
</div>,
|
</div>
|
||||||
<TopSection key="topsection" />,
|
<TopSection key="topsection" />
|
||||||
<BottomSection key="bottomsection" />,
|
<BottomSection key="bottomsection" />
|
||||||
];
|
</div>
|
||||||
}
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
SideMenu.displayName = 'SideMenu';
|
||||||
|
@ -2,6 +2,7 @@ import React, { FC } from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import DropDownChild from './DropDownChild';
|
import DropDownChild from './DropDownChild';
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
|
import { Link } from '@grafana/ui';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
link: NavModelItem;
|
link: NavModelItem;
|
||||||
@ -15,13 +16,20 @@ const SideMenuDropDown: FC<Props> = (props) => {
|
|||||||
childrenLinks = _.filter(link.children, (item) => !item.hideFromMenu);
|
childrenLinks = _.filter(link.children, (item) => !item.hideFromMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linkContent = <span className="sidemenu-item-text">{link.text}</span>;
|
||||||
|
const anchor = link.url ? (
|
||||||
|
<Link href={link.url} onClick={onHeaderClick} className="side-menu-header-link">
|
||||||
|
{linkContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a onClick={onHeaderClick} className="side-menu-header-link">
|
||||||
|
{linkContent}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
<li className="side-menu-header">
|
<li className="side-menu-header">{anchor}</li>
|
||||||
<a className="side-menu-header-link" href={link.url} onClick={onHeaderClick}>
|
|
||||||
<span className="sidemenu-item-text">{link.text}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{childrenLinks.map((child, index) => {
|
{childrenLinks.map((child, index) => {
|
||||||
return <DropDownChild child={child} key={`${child.url}-${index}`} />;
|
return <DropDownChild child={child} key={`${child.url}-${index}`} />;
|
||||||
})}
|
})}
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { SignIn } from './SignIn';
|
import { SignIn } from './SignIn';
|
||||||
|
import { Router } from 'react-router-dom';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
it('should render component', () => {
|
it('should render component', async () => {
|
||||||
const wrapper = shallow(<SignIn url="/whatever" />);
|
render(
|
||||||
|
<Router history={locationService.getHistory()}>
|
||||||
|
<SignIn url="/whatever" />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
const link = await screen.getByText('Sign In');
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
|
||||||
import { StoreState } from 'app/types';
|
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { getForcedLoginUrl } from './utils';
|
import { getForcedLoginUrl } from './utils';
|
||||||
|
|
||||||
export const SignIn: FC<any> = ({ url }) => {
|
export const SignIn: FC<any> = () => {
|
||||||
const forcedLoginUrl = getForcedLoginUrl(url);
|
const location = useLocation();
|
||||||
|
const forcedLoginUrl = getForcedLoginUrl(location.pathname + location.search);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidemenu-item">
|
<div className="sidemenu-item">
|
||||||
@ -25,9 +24,3 @@ export const SignIn: FC<any> = ({ url }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
|
||||||
url: state.location.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connectWithStore(SignIn, mapStateToProps);
|
|
||||||
|
@ -2,7 +2,7 @@ import React, { FC } from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import TopSectionItem from './TopSectionItem';
|
import TopSectionItem from './TopSectionItem';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { getLocationSrv } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
const TopSection: FC<any> = () => {
|
const TopSection: FC<any> = () => {
|
||||||
const navTree = _.cloneDeep(config.bootData.navTree);
|
const navTree = _.cloneDeep(config.bootData.navTree);
|
||||||
@ -13,7 +13,7 @@ const TopSection: FC<any> = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onOpenSearch = () => {
|
const onOpenSearch = () => {
|
||||||
getLocationSrv().update({ query: { search: 'open' }, partial: true });
|
locationService.partial({ search: 'open' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import TopSectionItem from './TopSectionItem';
|
import TopSectionItem from './TopSectionItem';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props = Object.assign(
|
const props = Object.assign(
|
||||||
@ -14,7 +15,11 @@ const setup = (propOverrides?: object) => {
|
|||||||
propOverrides
|
propOverrides
|
||||||
);
|
);
|
||||||
|
|
||||||
return mount(<TopSectionItem {...props} />);
|
return mount(
|
||||||
|
<MemoryRouter initialEntries={[{ pathname: '/', key: 'testKey' }]}>
|
||||||
|
<TopSectionItem {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import SideMenuDropDown from './SideMenuDropDown';
|
import SideMenuDropDown from './SideMenuDropDown';
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon, Link } from '@grafana/ui';
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -9,14 +9,25 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TopSectionItem: FC<Props> = ({ link, onClick }) => {
|
const TopSectionItem: FC<Props> = ({ link, onClick }) => {
|
||||||
return (
|
const linkContent = (
|
||||||
<div className="sidemenu-item dropdown">
|
|
||||||
<a className="sidemenu-link" href={link.url} target={link.target} onClick={onClick}>
|
|
||||||
<span className="icon-circle sidemenu-icon">
|
<span className="icon-circle sidemenu-icon">
|
||||||
{link.icon && <Icon name={link.icon as any} size="xl" />}
|
{link.icon && <Icon name={link.icon as any} size="xl" />}
|
||||||
{link.img && <img src={link.img} />}
|
{link.img && <img src={link.img} />}
|
||||||
</span>
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const anchor = link.url ? (
|
||||||
|
<Link className="sidemenu-link" href={link.url} target={link.target} onClick={onClick}>
|
||||||
|
{linkContent}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a className="sidemenu-link" onClick={onClick}>
|
||||||
|
{linkContent}
|
||||||
</a>
|
</a>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="sidemenu-item dropdown">
|
||||||
|
{anchor}
|
||||||
<SideMenuDropDown link={link} onHeaderClick={onClick} />
|
<SideMenuDropDown link={link} onHeaderClick={onClick} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -4,13 +4,13 @@ exports[`Render should render children 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="sidemenu-item dropdown dropup"
|
className="sidemenu-item dropdown dropup"
|
||||||
>
|
>
|
||||||
<a
|
<Link
|
||||||
className="sidemenu-link"
|
className="sidemenu-link"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="icon-circle sidemenu-icon"
|
className="icon-circle sidemenu-icon"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu dropdown-menu--sidemenu"
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
role="menu"
|
role="menu"
|
||||||
@ -58,13 +58,13 @@ exports[`Render should render component 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="sidemenu-item dropdown dropup"
|
className="sidemenu-item dropdown dropup"
|
||||||
>
|
>
|
||||||
<a
|
<Link
|
||||||
className="sidemenu-link"
|
className="sidemenu-link"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="icon-circle sidemenu-icon"
|
className="icon-circle sidemenu-icon"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu dropdown-menu--sidemenu"
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
role="menu"
|
role="menu"
|
||||||
@ -86,13 +86,13 @@ exports[`Render should render organization switcher 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="sidemenu-item dropdown dropup"
|
className="sidemenu-item dropdown dropup"
|
||||||
>
|
>
|
||||||
<a
|
<Link
|
||||||
className="sidemenu-link"
|
className="sidemenu-link"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="icon-circle sidemenu-icon"
|
className="icon-circle sidemenu-icon"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu dropdown-menu--sidemenu"
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
role="menu"
|
role="menu"
|
||||||
@ -144,13 +144,13 @@ exports[`Render should render subtitle 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="sidemenu-item dropdown dropup"
|
className="sidemenu-item dropdown dropup"
|
||||||
>
|
>
|
||||||
<a
|
<Link
|
||||||
className="sidemenu-link"
|
className="sidemenu-link"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="icon-circle sidemenu-icon"
|
className="icon-circle sidemenu-icon"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
<ul
|
<ul
|
||||||
className="dropdown-menu dropdown-menu--sidemenu"
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@ -4,7 +4,7 @@ exports[`Render should render component 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="sidemenu__bottom"
|
className="sidemenu__bottom"
|
||||||
>
|
>
|
||||||
<Component />
|
<SignIn />
|
||||||
<BottomNavLinks
|
<BottomNavLinks
|
||||||
key="undefined-0"
|
key="undefined-0"
|
||||||
link={
|
link={
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
|
||||||
Array [
|
|
||||||
<a
|
|
||||||
className="sidemenu__logo"
|
|
||||||
href="/"
|
|
||||||
key="logo"
|
|
||||||
>
|
|
||||||
<MenuLogo />
|
|
||||||
</a>,
|
|
||||||
<div
|
|
||||||
className="sidemenu__logo_small_breakpoint"
|
|
||||||
key="hamburger"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="bars"
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="sidemenu__close"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="times"
|
|
||||||
/>
|
|
||||||
Close
|
|
||||||
</span>
|
|
||||||
</div>,
|
|
||||||
<TopSection
|
|
||||||
key="topsection"
|
|
||||||
/>,
|
|
||||||
<BottomSection
|
|
||||||
key="bottomsection"
|
|
||||||
/>,
|
|
||||||
]
|
|
||||||
`;
|
|
@ -1,41 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
|
||||||
<div
|
|
||||||
className="sidemenu-item"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="sidemenu-link"
|
|
||||||
href="/whatever?forceLogin=true"
|
|
||||||
target="_self"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="icon-circle sidemenu-icon"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="signout"
|
|
||||||
size="xl"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/whatever?forceLogin=true"
|
|
||||||
target="_self"
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
className="dropdown-menu dropdown-menu--sidemenu"
|
|
||||||
role="menu"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
className="side-menu-header"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="sidemenu-item-text"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -1,6 +1,48 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
exports[`Render should render component 1`] = `
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "testKey",
|
||||||
|
"pathname": "/",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Router
|
||||||
|
history={
|
||||||
|
Object {
|
||||||
|
"action": "POP",
|
||||||
|
"block": [Function],
|
||||||
|
"canGo": [Function],
|
||||||
|
"createHref": [Function],
|
||||||
|
"entries": Array [
|
||||||
|
Object {
|
||||||
|
"hash": "",
|
||||||
|
"key": "testKey",
|
||||||
|
"pathname": "/",
|
||||||
|
"search": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"go": [Function],
|
||||||
|
"goBack": [Function],
|
||||||
|
"goForward": [Function],
|
||||||
|
"index": 0,
|
||||||
|
"length": 1,
|
||||||
|
"listen": [Function],
|
||||||
|
"location": Object {
|
||||||
|
"hash": "",
|
||||||
|
"key": "testKey",
|
||||||
|
"pathname": "/",
|
||||||
|
"search": "",
|
||||||
|
},
|
||||||
|
"push": [Function],
|
||||||
|
"replace": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<TopSectionItem
|
<TopSectionItem
|
||||||
link={
|
link={
|
||||||
Object {
|
Object {
|
||||||
@ -12,10 +54,24 @@ exports[`Render should render component 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="sidemenu-item dropdown"
|
className="sidemenu-item dropdown"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="sidemenu-link"
|
||||||
|
href="/asd"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="sidemenu-link"
|
||||||
|
to="/asd"
|
||||||
|
>
|
||||||
|
<LinkAnchor
|
||||||
|
className="sidemenu-link"
|
||||||
|
href="/asd"
|
||||||
|
navigate={[Function]}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="sidemenu-link"
|
className="sidemenu-link"
|
||||||
href="/asd"
|
href="/asd"
|
||||||
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="icon-circle sidemenu-icon"
|
className="icon-circle sidemenu-icon"
|
||||||
@ -49,6 +105,9 @@ exports[`Render should render component 1`] = `
|
|||||||
</Icon>
|
</Icon>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
</LinkAnchor>
|
||||||
|
</Link>
|
||||||
|
</Link>
|
||||||
<SideMenuDropDown
|
<SideMenuDropDown
|
||||||
link={
|
link={
|
||||||
Object {
|
Object {
|
||||||
@ -64,10 +123,24 @@ exports[`Render should render component 1`] = `
|
|||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="side-menu-header"
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="side-menu-header-link"
|
||||||
|
href="/asd"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="side-menu-header-link"
|
||||||
|
to="/asd"
|
||||||
|
>
|
||||||
|
<LinkAnchor
|
||||||
|
className="side-menu-header-link"
|
||||||
|
href="/asd"
|
||||||
|
navigate={[Function]}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="side-menu-header-link"
|
className="side-menu-header-link"
|
||||||
href="/asd"
|
href="/asd"
|
||||||
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="sidemenu-item-text"
|
className="sidemenu-item-text"
|
||||||
@ -75,9 +148,14 @@ exports[`Render should render component 1`] = `
|
|||||||
Hello
|
Hello
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
</LinkAnchor>
|
||||||
|
</Link>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</SideMenuDropDown>
|
</SideMenuDropDown>
|
||||||
</div>
|
</div>
|
||||||
</TopSectionItem>
|
</TopSectionItem>
|
||||||
|
</Router>
|
||||||
|
</MemoryRouter>
|
||||||
`;
|
`;
|
||||||
|
@ -1,4 +1 @@
|
|||||||
import './invited_ctrl';
|
|
||||||
import './signup_ctrl';
|
|
||||||
import './reset_password_ctrl';
|
|
||||||
import './json_editor_ctrl';
|
import './json_editor_ctrl';
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import coreModule from '../core_module';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import { promiseToDigest } from '../utils/promiseToDigest';
|
|
||||||
|
|
||||||
export class InvitedCtrl {
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope: any, $routeParams: any, contextSrv: any) {
|
|
||||||
contextSrv.sidemenu = false;
|
|
||||||
$scope.formModel = {};
|
|
||||||
|
|
||||||
$scope.navModel = {
|
|
||||||
main: {
|
|
||||||
icon: 'grafana',
|
|
||||||
text: 'Invite',
|
|
||||||
subTitle: 'Register your Grafana account',
|
|
||||||
breadcrumbs: [{ title: 'Login', url: 'login' }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.init = () => {
|
|
||||||
promiseToDigest($scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.get('/api/user/invite/' + $routeParams.code)
|
|
||||||
.then((invite: any) => {
|
|
||||||
$scope.formModel.name = invite.name;
|
|
||||||
$scope.formModel.email = invite.email;
|
|
||||||
$scope.formModel.username = invite.email;
|
|
||||||
$scope.formModel.inviteCode = $routeParams.code;
|
|
||||||
|
|
||||||
$scope.greeting = invite.name || invite.email || invite.username;
|
|
||||||
$scope.invitedBy = invite.invitedBy;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.submit = () => {
|
|
||||||
if (!$scope.inviteForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBackendSrv()
|
|
||||||
.post('/api/user/invite/complete', $scope.formModel)
|
|
||||||
.then(() => {
|
|
||||||
window.location.href = config.appSubUrl + '/';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('InvitedCtrl', InvitedCtrl);
|
|
@ -1,64 +0,0 @@
|
|||||||
import coreModule from '../core_module';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { AppEvents } from '@grafana/data';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import { promiseToDigest } from '../utils/promiseToDigest';
|
|
||||||
|
|
||||||
export class ResetPasswordCtrl {
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope: any, $location: any) {
|
|
||||||
$scope.formModel = {};
|
|
||||||
$scope.mode = 'send';
|
|
||||||
$scope.ldapEnabled = config.ldapEnabled;
|
|
||||||
$scope.authProxyEnabled = config.authProxyEnabled;
|
|
||||||
$scope.disableLoginForm = config.disableLoginForm;
|
|
||||||
|
|
||||||
const params = $location.search();
|
|
||||||
if (params.code) {
|
|
||||||
$scope.mode = 'reset';
|
|
||||||
$scope.formModel.code = params.code;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.navModel = {
|
|
||||||
main: {
|
|
||||||
icon: 'grafana',
|
|
||||||
text: 'Reset Password',
|
|
||||||
subTitle: 'Reset your Grafana password',
|
|
||||||
breadcrumbs: [{ title: 'Login', url: 'login' }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.sendResetEmail = () => {
|
|
||||||
if (!$scope.sendResetForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
promiseToDigest($scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.post('/api/user/password/send-reset-email', $scope.formModel)
|
|
||||||
.then(() => {
|
|
||||||
$scope.mode = 'email-sent';
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.submitReset = () => {
|
|
||||||
if (!$scope.resetForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($scope.formModel.newPassword !== $scope.formModel.confirmPassword) {
|
|
||||||
$scope.appEvent(AppEvents.alertWarning, ['New passwords do not match']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBackendSrv()
|
|
||||||
.post('/api/user/password/reset', $scope.formModel)
|
|
||||||
.then(() => {
|
|
||||||
$location.path('login');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('ResetPasswordCtrl', ResetPasswordCtrl);
|
|
@ -1,66 +0,0 @@
|
|||||||
import config from 'app/core/config';
|
|
||||||
import coreModule from '../core_module';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime/src/services';
|
|
||||||
import { promiseToDigest } from '../utils/promiseToDigest';
|
|
||||||
|
|
||||||
export class SignUpCtrl {
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private $scope: any, $location: any, contextSrv: any) {
|
|
||||||
contextSrv.sidemenu = false;
|
|
||||||
$scope.ctrl = this;
|
|
||||||
|
|
||||||
$scope.formModel = {};
|
|
||||||
|
|
||||||
const params = $location.search();
|
|
||||||
|
|
||||||
// validate email is semi ok
|
|
||||||
if (params.email && !params.email.match(/^\S+@\S+$/)) {
|
|
||||||
console.error('invalid email');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.formModel.orgName = params.email;
|
|
||||||
$scope.formModel.email = params.email;
|
|
||||||
$scope.formModel.username = params.email;
|
|
||||||
$scope.formModel.code = params.code;
|
|
||||||
|
|
||||||
$scope.verifyEmailEnabled = false;
|
|
||||||
$scope.autoAssignOrg = false;
|
|
||||||
|
|
||||||
$scope.navModel = {
|
|
||||||
main: {
|
|
||||||
icon: 'grafana',
|
|
||||||
text: 'Sign Up',
|
|
||||||
subTitle: 'Register your Grafana account',
|
|
||||||
breadcrumbs: [{ title: 'Login', url: 'login' }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
promiseToDigest($scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.get('/api/user/signup/options')
|
|
||||||
.then((options: any) => {
|
|
||||||
$scope.verifyEmailEnabled = options.verifyEmailEnabled;
|
|
||||||
$scope.autoAssignOrg = options.autoAssignOrg;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
submit() {
|
|
||||||
if (!this.$scope.signUpForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBackendSrv()
|
|
||||||
.post('/api/user/signup/step2', this.$scope.formModel)
|
|
||||||
.then((rsp: any) => {
|
|
||||||
if (rsp.code === 'redirect-to-select-org') {
|
|
||||||
window.location.href = config.appSubUrl + '/profile/select-org?signup=1';
|
|
||||||
} else {
|
|
||||||
window.location.href = config.appSubUrl + '/';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('SignUpCtrl', SignUpCtrl);
|
|
56
public/app/core/navigation/GrafanaRoute.test.tsx
Normal file
56
public/app/core/navigation/GrafanaRoute.test.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { GrafanaRoute } from './GrafanaRoute';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
|
describe('GrafanaRoute', () => {
|
||||||
|
it('Parses search', () => {
|
||||||
|
let capturedProps: any;
|
||||||
|
|
||||||
|
const PageComponent = (props: any) => {
|
||||||
|
capturedProps = props;
|
||||||
|
return <div />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const location = { search: '?query=hello&test=asd' } as any;
|
||||||
|
const history = {} as any;
|
||||||
|
const match = {} as any;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<GrafanaRoute location={location} history={history} match={match} route={{ component: PageComponent } as any} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(capturedProps.queryParams.query).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should clear history forceRouteReload state after route change', () => {
|
||||||
|
const renderSpy = jest.fn();
|
||||||
|
|
||||||
|
const route = {
|
||||||
|
/* eslint-disable-next-line react/display-name */
|
||||||
|
component: () => {
|
||||||
|
renderSpy();
|
||||||
|
return <div />;
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const history = locationService.getHistory();
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<GrafanaRoute location={history.location} history={history} match={{} as any} route={route} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(renderSpy).toBeCalledTimes(1);
|
||||||
|
locationService.replace('/test', true);
|
||||||
|
expect(history.location.state).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"forceRouteReload": true,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
rerender(<GrafanaRoute location={history.location} history={history} match={{} as any} route={route} />);
|
||||||
|
|
||||||
|
expect(history.location.state).toMatchInlineSnapshot(`Object {}`);
|
||||||
|
expect(renderSpy).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
83
public/app/core/navigation/GrafanaRoute.tsx
Normal file
83
public/app/core/navigation/GrafanaRoute.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// @ts-ignore
|
||||||
|
import Drop from 'tether-drop';
|
||||||
|
import { GrafanaRouteComponentProps } from './types';
|
||||||
|
import { locationSearchToObject, navigationLogger } from '@grafana/runtime';
|
||||||
|
import { keybindingSrv } from '../services/keybindingSrv';
|
||||||
|
import { shouldReloadPage } from './utils';
|
||||||
|
import { analyticsService } from '../services/analytics';
|
||||||
|
|
||||||
|
export interface Props extends Omit<GrafanaRouteComponentProps, 'queryParams'> {}
|
||||||
|
|
||||||
|
export class GrafanaRoute extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateBodyClassNames();
|
||||||
|
this.cleanupDOM();
|
||||||
|
|
||||||
|
// unbinds all and re-bind global keybindins
|
||||||
|
keybindingSrv.reset();
|
||||||
|
keybindingSrv.initGlobals();
|
||||||
|
analyticsService.track();
|
||||||
|
navigationLogger('GrafanaRoute', false, 'Mounted', this.props.match);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
this.cleanupDOM();
|
||||||
|
|
||||||
|
// Clear force reload state when route updates
|
||||||
|
if (shouldReloadPage(this.props.location)) {
|
||||||
|
navigationLogger('GrafanaRoute', false, 'Force reload', this.props, prevProps);
|
||||||
|
delete (this.props.history.location.state as any)?.forceRouteReload;
|
||||||
|
}
|
||||||
|
|
||||||
|
analyticsService.track();
|
||||||
|
navigationLogger('GrafanaRoute', false, 'Updated', this.props, prevProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.updateBodyClassNames(true);
|
||||||
|
navigationLogger('GrafanaRoute', false, 'Unmounted', this.props.route);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageClasses() {
|
||||||
|
return this.props.route.pageClass ? this.props.route.pageClass.split(' ') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBodyClassNames(clear = false) {
|
||||||
|
for (const cls of this.getPageClasses()) {
|
||||||
|
if (clear) {
|
||||||
|
document.body.classList.remove(cls);
|
||||||
|
} else {
|
||||||
|
document.body.classList.add(cls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupDOM() {
|
||||||
|
document.body.classList.remove('sidemenu-open--xs');
|
||||||
|
|
||||||
|
// cleanup tooltips
|
||||||
|
const tooltipById = document.getElementById('tooltip');
|
||||||
|
tooltipById?.parentElement?.removeChild(tooltipById);
|
||||||
|
|
||||||
|
const tooltipsByClass = document.querySelectorAll('.tooltip');
|
||||||
|
for (let i = 0; i < tooltipsByClass.length; i++) {
|
||||||
|
const tooltip = tooltipsByClass[i];
|
||||||
|
tooltip.parentElement?.removeChild(tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup tether-drop
|
||||||
|
for (const drop of Drop.drops) {
|
||||||
|
drop.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props } = this;
|
||||||
|
navigationLogger('GrafanaRoute', false, 'Rendered', props.route);
|
||||||
|
|
||||||
|
const RouteComponent = props.route.component;
|
||||||
|
|
||||||
|
return <RouteComponent {...props} queryParams={locationSearchToObject(props.location.search)} />;
|
||||||
|
}
|
||||||
|
}
|
45
public/app/core/navigation/RouterDebugger.tsx
Normal file
45
public/app/core/navigation/RouterDebugger.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { getAppRoutes } from '../../routes/routes';
|
||||||
|
import { PageContents } from '../components/Page/PageContents';
|
||||||
|
import { RouteDescriptor } from './types';
|
||||||
|
|
||||||
|
export const RouterDebugger: React.FC<any> = () => {
|
||||||
|
const manualRoutes: RouteDescriptor[] = [];
|
||||||
|
return (
|
||||||
|
<PageContents>
|
||||||
|
<h1>Static routes</h1>
|
||||||
|
<ul>
|
||||||
|
{getAppRoutes().map((r, i) => {
|
||||||
|
if (r.path.indexOf(':') > -1 || r.path.indexOf('test') > -1) {
|
||||||
|
if (r.path.indexOf('test') === -1) {
|
||||||
|
manualRoutes.push(r);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={i}>
|
||||||
|
<Link target="_blank" to={r.path}>
|
||||||
|
{r.path}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h1>Dynamic routes - check those manually</h1>
|
||||||
|
<ul>
|
||||||
|
{manualRoutes.map((r, i) => {
|
||||||
|
return (
|
||||||
|
<li key={i}>
|
||||||
|
<Link key={`${i}-${r.path}`} target="_blank" to={r.path}>
|
||||||
|
{r.path}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</PageContents>
|
||||||
|
);
|
||||||
|
};
|
19
public/app/core/navigation/__mocks__/routeProps.ts
Normal file
19
public/app/core/navigation/__mocks__/routeProps.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { GrafanaRouteComponentProps } from '../types';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { merge } from 'lodash';
|
||||||
|
|
||||||
|
export function getRouteComponentProps<T = {}, Q extends Record<string, string | null | undefined> = {}>(
|
||||||
|
overrides: Partial<GrafanaRouteComponentProps> = {}
|
||||||
|
): GrafanaRouteComponentProps<T, Q> {
|
||||||
|
const defaults: GrafanaRouteComponentProps<T, Q> = {
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
location: {
|
||||||
|
search: '',
|
||||||
|
} as any,
|
||||||
|
match: { params: {} } as any,
|
||||||
|
route: {} as any,
|
||||||
|
queryParams: {} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
return merge(overrides, defaults);
|
||||||
|
}
|
17
public/app/core/navigation/hooks.ts
Normal file
17
public/app/core/navigation/hooks.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export type UseUrlParamsResult = [URLSearchParams, (params: Record<string, any>, replace?: boolean) => void];
|
||||||
|
|
||||||
|
/** @internal experimental */
|
||||||
|
export function useUrlParams(): UseUrlParamsResult {
|
||||||
|
const location = useLocation();
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
const updateUrlParams = (params: Record<string, any>, replace?: boolean) => {
|
||||||
|
// Should find a way to use history directly here
|
||||||
|
locationService.partial(params, replace);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [params, updateUrlParams];
|
||||||
|
}
|
40
public/app/core/navigation/kiosk.ts
Normal file
40
public/app/core/navigation/kiosk.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { AppEvents, UrlQueryValue } from '@grafana/data';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import appEvents from '../app_events';
|
||||||
|
import { KioskMode } from '../../types';
|
||||||
|
|
||||||
|
export function toggleKioskMode() {
|
||||||
|
let kiosk = locationService.getSearchObject().kiosk;
|
||||||
|
|
||||||
|
switch (kiosk) {
|
||||||
|
case 'tv':
|
||||||
|
kiosk = true;
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, ['Press ESC to exit Kiosk mode']);
|
||||||
|
break;
|
||||||
|
case '1':
|
||||||
|
case true:
|
||||||
|
kiosk = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
kiosk = 'tv';
|
||||||
|
}
|
||||||
|
|
||||||
|
locationService.partial({ kiosk });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKioskMode(queryParam?: UrlQueryValue): KioskMode {
|
||||||
|
switch (queryParam) {
|
||||||
|
case 'tv':
|
||||||
|
return KioskMode.TV;
|
||||||
|
// legacy support
|
||||||
|
case '1':
|
||||||
|
case true:
|
||||||
|
return KioskMode.Full;
|
||||||
|
default:
|
||||||
|
return KioskMode.Off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitKioskMode() {
|
||||||
|
locationService.partial({ kiosk: null });
|
||||||
|
}
|
165
public/app/core/navigation/parseKeyValue.ts
Normal file
165
public/app/core/navigation/parseKeyValue.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// tslint:disable
|
||||||
|
// Most of this file is just a copy of some content from
|
||||||
|
// https://github.com/angular/angular.js/blob/937fb891fa4fcf79e9fa02f8e0d517593e781077/src/Angular.js
|
||||||
|
// Long term this code should be refactored to tru-type-script ;)
|
||||||
|
// tslint disabled on purpose
|
||||||
|
|
||||||
|
const getPrototypeOf = Object.getPrototypeOf;
|
||||||
|
const toString = Object.prototype.toString;
|
||||||
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
let jqLite: any;
|
||||||
|
|
||||||
|
export function isArray(arr: any) {
|
||||||
|
return Array.isArray(arr) || arr instanceof Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isError(value: any) {
|
||||||
|
var tag = toString.call(value);
|
||||||
|
switch (tag) {
|
||||||
|
case '[object Error]':
|
||||||
|
return true;
|
||||||
|
case '[object Exception]':
|
||||||
|
return true;
|
||||||
|
case '[object DOMException]':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return value instanceof Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function isDate(value: any) {
|
||||||
|
return toString.call(value) === '[object Date]';
|
||||||
|
}
|
||||||
|
export function isNumber(value: any) {
|
||||||
|
return typeof value === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isString(value: any) {
|
||||||
|
return typeof value === 'string';
|
||||||
|
}
|
||||||
|
export function isBlankObject(value: any) {
|
||||||
|
return value !== null && typeof value === 'object' && !getPrototypeOf(value);
|
||||||
|
}
|
||||||
|
export function isObject(value: any) {
|
||||||
|
// http://jsperf.com/isobject4
|
||||||
|
return value !== null && typeof value === 'object';
|
||||||
|
}
|
||||||
|
export function isDefined(value: any) {
|
||||||
|
return typeof value !== 'undefined';
|
||||||
|
}
|
||||||
|
export function isWindow(obj: { window: any }) {
|
||||||
|
return obj && obj.window === obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArrayLike(obj: any) {
|
||||||
|
// `null`, `undefined` and `window` are not array-like
|
||||||
|
if (obj == null || isWindow(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrays, strings and jQuery/jqLite objects are array like
|
||||||
|
// * jqLite is either the jQuery or jqLite constructor function
|
||||||
|
// * we have to check the existence of jqLite first as this method is called
|
||||||
|
// via the forEach method when constructing the jqLite object in the first place
|
||||||
|
if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support: iOS 8.2 (not reproducible in simulator)
|
||||||
|
// "length" in obj used to prevent JIT error (gh-11508)
|
||||||
|
var length = 'length' in Object(obj) && obj.length;
|
||||||
|
|
||||||
|
// NodeList objects (with `item` method) and
|
||||||
|
// other objects with suitable length characteristics are array-like
|
||||||
|
return isNumber(length) && ((length >= 0 && length - 1 in obj) || typeof obj.item === 'function');
|
||||||
|
}
|
||||||
|
export function isFunction(value: any) {
|
||||||
|
return typeof value === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forEach(obj: any, iterator: any, context?: any) {
|
||||||
|
var key, length;
|
||||||
|
if (obj) {
|
||||||
|
if (isFunction(obj)) {
|
||||||
|
for (key in obj) {
|
||||||
|
if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) {
|
||||||
|
iterator.call(context, obj[key], key, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isArray(obj) || isArrayLike(obj)) {
|
||||||
|
var isPrimitive = typeof obj !== 'object';
|
||||||
|
for (key = 0, length = obj.length; key < length; key++) {
|
||||||
|
if (isPrimitive || key in obj) {
|
||||||
|
iterator.call(context, obj[key], key, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj.forEach && obj.forEach !== forEach) {
|
||||||
|
obj.forEach(iterator, context, obj);
|
||||||
|
} else if (isBlankObject(obj)) {
|
||||||
|
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
|
||||||
|
for (key in obj) {
|
||||||
|
iterator.call(context, obj[key], key, obj);
|
||||||
|
}
|
||||||
|
} else if (typeof obj.hasOwnProperty === 'function') {
|
||||||
|
// Slow path for objects inheriting Object.prototype, hasOwnProperty check needed
|
||||||
|
for (key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
iterator.call(context, obj[key], key, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Slow path for objects which do not have a method `hasOwnProperty`
|
||||||
|
for (key in obj) {
|
||||||
|
if (hasOwnProperty.call(obj, key)) {
|
||||||
|
iterator.call(context, obj[key], key, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
export function tryDecodeURIComponent(value: string): string | void {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore any invalid uri component.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseKeyValue(keyValue: string | null) {
|
||||||
|
var obj = {};
|
||||||
|
forEach((keyValue || '').split('&'), function (keyValue: string) {
|
||||||
|
var splitPoint, key, val;
|
||||||
|
if (keyValue) {
|
||||||
|
key = keyValue = keyValue.replace(/\+/g, '%20');
|
||||||
|
splitPoint = keyValue.indexOf('=');
|
||||||
|
if (splitPoint !== -1) {
|
||||||
|
key = keyValue.substring(0, splitPoint);
|
||||||
|
val = keyValue.substring(splitPoint + 1);
|
||||||
|
}
|
||||||
|
key = tryDecodeURIComponent(key);
|
||||||
|
if (isDefined(key)) {
|
||||||
|
val = isDefined(val) ? tryDecodeURIComponent(val as string) : true;
|
||||||
|
if (!hasOwnProperty.call(obj, key)) {
|
||||||
|
// @ts-ignore
|
||||||
|
obj[key] = val;
|
||||||
|
// @ts-ignore
|
||||||
|
} else if (isArray(obj[key])) {
|
||||||
|
// @ts-ignore
|
||||||
|
obj[key].push(val);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
obj[key] = [obj[key], val];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
export function isUndefined(value: any) {
|
||||||
|
return typeof value === 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parseKeyValue;
|
||||||
|
|
||||||
|
// tslint:enable
|
13
public/app/core/navigation/patch/RouteParamsProvider.ts
Normal file
13
public/app/core/navigation/patch/RouteParamsProvider.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// This is empty for now, as I think it's not going to be necessary.
|
||||||
|
// This replaces Angular RouteParamsProvider implementation with a dummy one to keep the ball rolling
|
||||||
|
|
||||||
|
import { navigationLogger } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export class RouteParamsProvider {
|
||||||
|
constructor() {
|
||||||
|
navigationLogger('Patch angular', false, 'RouteParamsProvider');
|
||||||
|
}
|
||||||
|
$get = () => {
|
||||||
|
// throw new Error('TODO: Refactor $routeParams');
|
||||||
|
};
|
||||||
|
}
|
14
public/app/core/navigation/patch/RouteProvider.ts
Normal file
14
public/app/core/navigation/patch/RouteProvider.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// This is empty for now, as I think it's not going to be necessary.
|
||||||
|
// This replaces Angular RouteProvider implementation with a dummy one to keep the ball rolling
|
||||||
|
|
||||||
|
import { navigationLogger } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export class RouteProvider {
|
||||||
|
constructor() {
|
||||||
|
navigationLogger('Patch angular', false, 'RouteProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
$get() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
41
public/app/core/navigation/patch/interceptLinkClicks.ts
Normal file
41
public/app/core/navigation/patch/interceptLinkClicks.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { locationUtil } from '@grafana/data';
|
||||||
|
import { locationService, navigationLogger } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export function interceptLinkClicks(e: MouseEvent) {
|
||||||
|
const anchor = getParentAnchor(e.target as HTMLElement);
|
||||||
|
|
||||||
|
// Ignore if opening new tab or already default prevented
|
||||||
|
if (e.ctrlKey || e.metaKey || e.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor) {
|
||||||
|
let href = anchor.getAttribute('href');
|
||||||
|
const target = anchor.getAttribute('target');
|
||||||
|
|
||||||
|
if (href && !target) {
|
||||||
|
navigationLogger('utils', false, 'intercepting link click', e);
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
href = locationUtil.stripBaseFromUrl(href);
|
||||||
|
// Ensure old angular urls with no starting '/' are handled the same as before
|
||||||
|
// That is they where seen as being absolute from app root
|
||||||
|
if (href[0] !== '/') {
|
||||||
|
href = `/${href}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
locationService.push(href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentAnchor(element: HTMLElement | null): HTMLElement | null {
|
||||||
|
while (element !== null && element.tagName) {
|
||||||
|
if (element.tagName.toUpperCase() === 'A') {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
element = element.parentNode as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
10
public/app/core/navigation/queryString.ts
Normal file
10
public/app/core/navigation/queryString.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const queryString = (params: any) => {
|
||||||
|
return Object.keys(params)
|
||||||
|
.filter((k) => {
|
||||||
|
return !!params[k];
|
||||||
|
})
|
||||||
|
.map((k) => {
|
||||||
|
return k + '=' + params[k];
|
||||||
|
})
|
||||||
|
.join('&');
|
||||||
|
};
|
35
public/app/core/navigation/testRoutes.tsx
Normal file
35
public/app/core/navigation/testRoutes.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
|
import { RouterDebugger } from './RouterDebugger';
|
||||||
|
import { RouteDescriptor } from './types';
|
||||||
|
|
||||||
|
export const testRoutes: RouteDescriptor[] = [
|
||||||
|
{
|
||||||
|
path: '/test1',
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
component: () =>
|
||||||
|
(
|
||||||
|
<>
|
||||||
|
<h1>Test1</h1>
|
||||||
|
<Link to={'/test2'}>Test2 link</Link>
|
||||||
|
<NavLink to={'/test2'}>Test2 navlink</NavLink>
|
||||||
|
</>
|
||||||
|
) as any,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test2',
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
component: () => (
|
||||||
|
<>
|
||||||
|
<h1>Test2 </h1>
|
||||||
|
<Link to={'/test1'}>Test1 link</Link>
|
||||||
|
<NavLink to={'/test1'}>Test1 navlink</NavLink>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/router-debug',
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
component: () => RouterDebugger,
|
||||||
|
},
|
||||||
|
];
|
19
public/app/core/navigation/types.ts
Normal file
19
public/app/core/navigation/types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { UrlQueryMap } from '@grafana/data';
|
||||||
|
import React from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface GrafanaRouteComponentProps<T = {}, Q = UrlQueryMap> extends RouteComponentProps<T> {
|
||||||
|
route: RouteDescriptor;
|
||||||
|
queryParams: Q;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GrafanaRouteComponent<T = any> = React.ComponentType<GrafanaRouteComponentProps<T>>;
|
||||||
|
|
||||||
|
export interface RouteDescriptor {
|
||||||
|
path: string;
|
||||||
|
component: GrafanaRouteComponent<any>;
|
||||||
|
roles?: () => string[];
|
||||||
|
pageClass?: string;
|
||||||
|
/** Can be used like an id for the route if the same component is used by many routes */
|
||||||
|
routeName?: string;
|
||||||
|
}
|
5
public/app/core/navigation/utils.ts
Normal file
5
public/app/core/navigation/utils.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as H from 'history';
|
||||||
|
|
||||||
|
export function shouldReloadPage(location: H.Location<any>) {
|
||||||
|
return !!location.state?.forceRouteReload;
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
import { navIndexReducer as navIndex } from './navModel';
|
import { navIndexReducer as navIndex } from './navModel';
|
||||||
import { locationReducer as location } from './location';
|
|
||||||
import { appNotificationsReducer as appNotifications } from './appNotification';
|
import { appNotificationsReducer as appNotifications } from './appNotification';
|
||||||
import { applicationReducer as application } from './application';
|
import { applicationReducer as application } from './application';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
navIndex,
|
navIndex,
|
||||||
location,
|
|
||||||
appNotifications,
|
appNotifications,
|
||||||
application,
|
application,
|
||||||
};
|
};
|
||||||
|
@ -1,154 +0,0 @@
|
|||||||
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
|
||||||
import { initialState, locationReducer, updateLocation } from './location';
|
|
||||||
import { LocationState } from '../../types';
|
|
||||||
|
|
||||||
describe('locationReducer', () => {
|
|
||||||
describe('when updateLocation is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LocationState>()
|
|
||||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
updateLocation({
|
|
||||||
query: { queryParam: 1 },
|
|
||||||
partial: false,
|
|
||||||
path: '/api/dashboard',
|
|
||||||
replace: false,
|
|
||||||
routeParams: { routeParam: 2 },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStatePredicateShouldEqual((resultingState) => {
|
|
||||||
expect(resultingState.path).toEqual('/api/dashboard');
|
|
||||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1');
|
|
||||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
|
||||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
|
||||||
expect(resultingState.replace).toEqual(false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when updateLocation is dispatched with replace', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LocationState>()
|
|
||||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
updateLocation({
|
|
||||||
query: { queryParam: 1 },
|
|
||||||
partial: false,
|
|
||||||
path: '/api/dashboard',
|
|
||||||
replace: true,
|
|
||||||
routeParams: { routeParam: 2 },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStatePredicateShouldEqual((resultingState) => {
|
|
||||||
expect(resultingState.path).toEqual('/api/dashboard');
|
|
||||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1');
|
|
||||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
|
||||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
|
||||||
expect(resultingState.replace).toEqual(true);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when updateLocation is dispatched with partial', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LocationState>()
|
|
||||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
updateLocation({
|
|
||||||
query: { queryParam: 1 },
|
|
||||||
partial: true,
|
|
||||||
path: '/api/dashboard',
|
|
||||||
replace: false,
|
|
||||||
routeParams: { routeParam: 2 },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStatePredicateShouldEqual((resultingState) => {
|
|
||||||
expect(resultingState.path).toEqual('/api/dashboard');
|
|
||||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1&queryParam2=2');
|
|
||||||
expect(resultingState.query).toEqual({ queryParam: 1, queryParam2: 2 });
|
|
||||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
|
||||||
expect(resultingState.replace).toEqual(false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when updateLocation is dispatched without query', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LocationState>()
|
|
||||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
updateLocation({
|
|
||||||
partial: false,
|
|
||||||
path: '/api/dashboard',
|
|
||||||
replace: false,
|
|
||||||
routeParams: { routeParam: 2 },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStatePredicateShouldEqual((resultingState) => {
|
|
||||||
expect(resultingState.path).toEqual('/api/dashboard');
|
|
||||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=3&queryParam2=2');
|
|
||||||
expect(resultingState.query).toEqual({ queryParam: 3, queryParam2: 2 });
|
|
||||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
|
||||||
expect(resultingState.replace).toEqual(false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when updateLocation is dispatched without routeParams', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LocationState>()
|
|
||||||
.givenReducer(locationReducer, {
|
|
||||||
...initialState,
|
|
||||||
query: { queryParam: 3, queryParam2: 2 },
|
|
||||||
routeParams: { routeStateParam: 4 },
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
updateLocation({
|
|
||||||
query: { queryParam: 1 },
|
|
||||||
partial: false,
|
|
||||||
path: '/api/dashboard',
|
|
||||||
replace: false,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStatePredicateShouldEqual((resultingState) => {
|
|
||||||
expect(resultingState.path).toEqual('/api/dashboard');
|
|
||||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1');
|
|
||||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
|
||||||
expect(resultingState.routeParams).toEqual({ routeStateParam: 4 });
|
|
||||||
expect(resultingState.replace).toEqual(false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when updateLocation is dispatched without path', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LocationState>()
|
|
||||||
.givenReducer(locationReducer, {
|
|
||||||
...initialState,
|
|
||||||
query: { queryParam: 3, queryParam2: 2 },
|
|
||||||
path: '/api/state/path',
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(
|
|
||||||
updateLocation({
|
|
||||||
query: { queryParam: 1 },
|
|
||||||
partial: false,
|
|
||||||
replace: false,
|
|
||||||
routeParams: { routeParam: 2 },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.thenStatePredicateShouldEqual((resultingState) => {
|
|
||||||
expect(resultingState.path).toEqual('/api/state/path');
|
|
||||||
expect(resultingState.url).toEqual('/api/state/path?queryParam=1');
|
|
||||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
|
||||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
|
||||||
expect(resultingState.replace).toEqual(false);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,46 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { Action, createAction } from '@reduxjs/toolkit';
|
|
||||||
import { LocationUpdate } from '@grafana/runtime';
|
|
||||||
|
|
||||||
import { LocationState } from 'app/types';
|
|
||||||
import { urlUtil } from '@grafana/data';
|
|
||||||
|
|
||||||
export const initialState: LocationState = {
|
|
||||||
url: '',
|
|
||||||
path: '',
|
|
||||||
query: {},
|
|
||||||
routeParams: {},
|
|
||||||
replace: false,
|
|
||||||
lastUpdated: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateLocation = createAction<LocationUpdate>('location/updateLocation');
|
|
||||||
|
|
||||||
// Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
|
|
||||||
// ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
|
|
||||||
// because the state would become frozen and during run time we would get errors because Angular would try to mutate
|
|
||||||
// the frozen state.
|
|
||||||
// https://github.com/reduxjs/redux-toolkit/issues/242
|
|
||||||
export const locationReducer = (state: LocationState = initialState, action: Action<unknown>) => {
|
|
||||||
if (updateLocation.match(action)) {
|
|
||||||
const payload: LocationUpdate = action.payload;
|
|
||||||
const { path, routeParams, replace } = payload;
|
|
||||||
let query = payload.query || state.query;
|
|
||||||
|
|
||||||
if (payload.partial) {
|
|
||||||
query = _.defaults(query, state.query);
|
|
||||||
query = _.omitBy(query, _.isNull);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: urlUtil.renderUrl(path || state.path, query),
|
|
||||||
path: path || state.path,
|
|
||||||
query: { ...query },
|
|
||||||
routeParams: routeParams || state.routeParams,
|
|
||||||
replace: replace === true,
|
|
||||||
lastUpdated: new Date().getTime(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
@ -1,7 +0,0 @@
|
|||||||
import { LocationState } from 'app/types';
|
|
||||||
|
|
||||||
export const getRouteParamsId = (state: LocationState) => state.routeParams.id;
|
|
||||||
export const getRouteParamsPage = (state: LocationState) => state.routeParams.page;
|
|
||||||
export const getRouteParams = (state: LocationState) => state.routeParams;
|
|
||||||
export const getLocationQuery = (state: LocationState) => state.query;
|
|
||||||
export const getUrl = (state: LocationState) => state.url;
|
|
@ -7,4 +7,3 @@ import './popover_srv';
|
|||||||
import './segment_srv';
|
import './segment_srv';
|
||||||
import './backend_srv';
|
import './backend_srv';
|
||||||
import './dynamic_directive_srv';
|
import './dynamic_directive_srv';
|
||||||
import './bridge_srv';
|
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
export class Analytics {
|
export class Analytics {
|
||||||
/** @ngInject */
|
private gaId?: string;
|
||||||
constructor(private $rootScope: GrafanaRootScope, private $location: any) {}
|
private ga?: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.track = this.track.bind(this);
|
||||||
|
this.gaId = (config as any).googleAnalyticsId;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (!this.gaId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
gaInit() {
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'https://www.google-analytics.com/analytics.js',
|
url: 'https://www.google-analytics.com/analytics.js',
|
||||||
dataType: 'script',
|
dataType: 'script',
|
||||||
cache: true,
|
cache: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ga = ((window as any).ga =
|
const ga = ((window as any).ga =
|
||||||
(window as any).ga ||
|
(window as any).ga ||
|
||||||
// this had the equivalent of `eslint-disable-next-line prefer-arrow/prefer-arrow-functions`
|
// this had the equivalent of `eslint-disable-next-line prefer-arrow/prefer-arrow-functions`
|
||||||
@ -22,24 +32,21 @@ export class Analytics {
|
|||||||
ga.l = +new Date();
|
ga.l = +new Date();
|
||||||
ga('create', (config as any).googleAnalyticsId, 'auto');
|
ga('create', (config as any).googleAnalyticsId, 'auto');
|
||||||
ga('set', 'anonymizeIp', true);
|
ga('set', 'anonymizeIp', true);
|
||||||
|
this.ga = ga;
|
||||||
return ga;
|
return ga;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
track() {
|
||||||
this.$rootScope.$on('$viewContentLoaded', () => {
|
if (!this.ga) {
|
||||||
const track = { page: `${config.appSubUrl ?? ''}${this.$location.url()}` };
|
return;
|
||||||
const ga = (window as any).ga || this.gaInit();
|
}
|
||||||
ga('set', track);
|
|
||||||
ga('send', 'pageview');
|
const location = locationService.getLocation();
|
||||||
});
|
const track = { page: `${config.appSubUrl ?? ''}${location.pathname}${location.search}${location.hash}` };
|
||||||
|
|
||||||
|
this.ga('set', track);
|
||||||
|
this.ga('send', 'pageview');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
export const analyticsService = new Analytics();
|
||||||
function startAnalytics(googleAnalyticsSrv: Analytics) {
|
|
||||||
if ((config as any).googleAnalyticsId) {
|
|
||||||
googleAnalyticsSrv.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.service('googleAnalyticsSrv', Analytics).run(startAnalytics);
|
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
import { UrlQueryMap } from '@grafana/data';
|
|
||||||
import { findTemplateVarChanges } from './bridge_srv';
|
|
||||||
|
|
||||||
describe('when checking template variables', () => {
|
|
||||||
it('detect adding/removing a variable', () => {
|
|
||||||
const a: UrlQueryMap = {};
|
|
||||||
const b: UrlQueryMap = {
|
|
||||||
'var-xyz': 'hello',
|
|
||||||
aaa: 'ignore me',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(findTemplateVarChanges(b, a)).toEqual({ 'var-xyz': 'hello' });
|
|
||||||
expect(findTemplateVarChanges(a, b)).toEqual({ 'var-xyz': '' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('then should ignore equal values', () => {
|
|
||||||
const a: UrlQueryMap = {
|
|
||||||
'var-xyz': 'hello',
|
|
||||||
bbb: 'ignore me',
|
|
||||||
};
|
|
||||||
const b: UrlQueryMap = {
|
|
||||||
'var-xyz': 'hello',
|
|
||||||
aaa: 'ignore me',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(findTemplateVarChanges(b, a)).toBeUndefined();
|
|
||||||
expect(findTemplateVarChanges(a, b)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('then should ignore equal values with empty values', () => {
|
|
||||||
const a: UrlQueryMap = {
|
|
||||||
'var-xyz': '',
|
|
||||||
bbb: 'ignore me',
|
|
||||||
};
|
|
||||||
const b: UrlQueryMap = {
|
|
||||||
'var-xyz': '',
|
|
||||||
aaa: 'ignore me',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(findTemplateVarChanges(b, a)).toBeUndefined();
|
|
||||||
expect(findTemplateVarChanges(a, b)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('then should ignore empty array values', () => {
|
|
||||||
const a: UrlQueryMap = {
|
|
||||||
'var-adhoc': [],
|
|
||||||
};
|
|
||||||
const b: UrlQueryMap = {};
|
|
||||||
|
|
||||||
expect(findTemplateVarChanges(b, a)).toBeUndefined();
|
|
||||||
expect(findTemplateVarChanges(a, b)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should handle array values with one value same as just value', () => {
|
|
||||||
const a: UrlQueryMap = {
|
|
||||||
'var-test': ['test'],
|
|
||||||
};
|
|
||||||
const b: UrlQueryMap = {
|
|
||||||
'var-test': 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(findTemplateVarChanges(b, a)).toBeUndefined();
|
|
||||||
expect(findTemplateVarChanges(a, b)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should detect change in array value and return array with single value', () => {
|
|
||||||
const a: UrlQueryMap = {
|
|
||||||
'var-test': ['test'],
|
|
||||||
};
|
|
||||||
const b: UrlQueryMap = {
|
|
||||||
'var-test': 'asd',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(findTemplateVarChanges(a, b)!['var-test']).toEqual(['test']);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,152 +0,0 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import { dispatch, store } from 'app/store/store';
|
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import { ILocationService, ITimeoutService } from 'angular';
|
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
|
||||||
import { UrlQueryMap } from '@grafana/data';
|
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
|
||||||
import { templateVarsChangedInUrl } from 'app/features/variables/state/actions';
|
|
||||||
import { isArray, isEqual } from 'lodash';
|
|
||||||
|
|
||||||
// Services that handles angular -> redux store sync & other react <-> angular sync
|
|
||||||
export class BridgeSrv {
|
|
||||||
private lastQuery: UrlQueryMap = {};
|
|
||||||
private lastPath = '';
|
|
||||||
private angularUrl: string;
|
|
||||||
private lastUrl: string | null = null;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(
|
|
||||||
private $location: ILocationService,
|
|
||||||
private $timeout: ITimeoutService,
|
|
||||||
private $rootScope: GrafanaRootScope,
|
|
||||||
private $route: any
|
|
||||||
) {
|
|
||||||
this.angularUrl = $location.url();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.$rootScope.$on('$routeUpdate', (evt, data) => {
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
this.angularUrl = this.$location.url();
|
|
||||||
|
|
||||||
if (state.location.url !== this.angularUrl) {
|
|
||||||
store.dispatch(
|
|
||||||
updateLocation({
|
|
||||||
path: this.$location.path(),
|
|
||||||
query: this.$location.search(),
|
|
||||||
routeParams: this.$route.current.params,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
|
|
||||||
this.angularUrl = this.$location.url();
|
|
||||||
|
|
||||||
store.dispatch(
|
|
||||||
updateLocation({
|
|
||||||
path: this.$location.path(),
|
|
||||||
query: this.$location.search(),
|
|
||||||
routeParams: this.$route.current.params,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for changes in redux location -> update angular location
|
|
||||||
store.subscribe(() => {
|
|
||||||
const state = store.getState();
|
|
||||||
const url = state.location.url;
|
|
||||||
|
|
||||||
// No url change ignore redux store change
|
|
||||||
if (url === this.lastUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.angularUrl !== url) {
|
|
||||||
// store angular url right away as otherwise we end up syncing multiple times
|
|
||||||
this.angularUrl = url;
|
|
||||||
|
|
||||||
this.$timeout(() => {
|
|
||||||
this.$location.url(url);
|
|
||||||
// some state changes should not trigger new browser history
|
|
||||||
if (state.location.replace) {
|
|
||||||
this.$location.replace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// if only query params changed, check if variables changed
|
|
||||||
if (state.location.path === this.lastPath && state.location.query !== this.lastQuery) {
|
|
||||||
// Find template variable changes
|
|
||||||
const changes = findTemplateVarChanges(state.location.query, this.lastQuery);
|
|
||||||
// Store current query params to avoid recursion
|
|
||||||
this.lastQuery = state.location.query;
|
|
||||||
|
|
||||||
if (changes) {
|
|
||||||
const dash = getDashboardSrv().getCurrent();
|
|
||||||
if (dash) {
|
|
||||||
dispatch(templateVarsChangedInUrl(changes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastPath = state.location.path;
|
|
||||||
this.lastQuery = state.location.query;
|
|
||||||
this.lastUrl = state.location.url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUrlValueForComparison(value: any): any {
|
|
||||||
if (isArray(value)) {
|
|
||||||
if (value.length === 0) {
|
|
||||||
value = undefined;
|
|
||||||
} else if (value.length === 1) {
|
|
||||||
value = value[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findTemplateVarChanges(query: UrlQueryMap, old: UrlQueryMap): UrlQueryMap | undefined {
|
|
||||||
let count = 0;
|
|
||||||
const changes: UrlQueryMap = {};
|
|
||||||
|
|
||||||
for (const key in query) {
|
|
||||||
if (!key.startsWith('var-')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldValue = getUrlValueForComparison(old[key]);
|
|
||||||
let newValue = getUrlValueForComparison(query[key]);
|
|
||||||
|
|
||||||
if (!isEqual(newValue, oldValue)) {
|
|
||||||
changes[key] = query[key];
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in old) {
|
|
||||||
if (!key.startsWith('var-')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = old[key];
|
|
||||||
|
|
||||||
// ignore empty array values
|
|
||||||
if (isArray(value) && value.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!query.hasOwnProperty(key)) {
|
|
||||||
changes[key] = ''; // removed
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count ? changes : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.service('bridgeSrv', BridgeSrv);
|
|
@ -1,47 +1,39 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import 'mousetrap-global-bind';
|
import 'mousetrap-global-bind';
|
||||||
import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
|
|
||||||
import { LegacyGraphHoverClearEvent, locationUtil } from '@grafana/data';
|
import { LegacyGraphHoverClearEvent, locationUtil } from '@grafana/data';
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { getExploreUrl } from 'app/core/utils/explore';
|
import { getExploreUrl } from 'app/core/utils/explore';
|
||||||
import { dispatch, store } from 'app/store/store';
|
|
||||||
import { exitPanelEditor } from 'app/features/dashboard/components/PanelEditor/state/actions';
|
|
||||||
import { AppEventEmitter, CoreEvents } from 'app/types';
|
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
||||||
import { SaveDashboardModalProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
|
import { SaveDashboardModalProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
|
||||||
import { defaultQueryParams } from 'app/features/search/reducers/searchQueryReducer';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { ContextSrv } from './context_srv';
|
import { exitKioskMode, toggleKioskMode } from '../navigation/kiosk';
|
||||||
|
import {
|
||||||
|
HideModalEvent,
|
||||||
|
RemovePanelEvent,
|
||||||
|
ShiftTimeEvent,
|
||||||
|
ShiftTimeEventPayload,
|
||||||
|
ShowModalEvent,
|
||||||
|
ShowModalReactEvent,
|
||||||
|
ZoomOutEvent,
|
||||||
|
} from '../../types/events';
|
||||||
|
import { contextSrv } from '../core';
|
||||||
|
import { getDatasourceSrv } from '../../features/plugins/datasource_srv';
|
||||||
|
import { getTimeSrv } from '../../features/dashboard/services/TimeSrv';
|
||||||
|
|
||||||
export class KeybindingSrv {
|
export class KeybindingSrv {
|
||||||
modalOpen = false;
|
modalOpen = false;
|
||||||
|
|
||||||
/** @ngInject */
|
constructor() {
|
||||||
constructor(
|
appEvents.subscribe(ShowModalEvent, () => (this.modalOpen = true));
|
||||||
private $rootScope: GrafanaRootScope,
|
|
||||||
private $location: ILocationService,
|
|
||||||
private $timeout: ITimeoutService,
|
|
||||||
private datasourceSrv: any,
|
|
||||||
private timeSrv: any,
|
|
||||||
private contextSrv: ContextSrv
|
|
||||||
) {
|
|
||||||
// clear out all shortcuts on route change
|
|
||||||
$rootScope.$on('$routeChangeSuccess', () => {
|
|
||||||
Mousetrap.reset();
|
|
||||||
// rebind global shortcuts
|
|
||||||
this.setupGlobal();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setupGlobal();
|
|
||||||
appEvents.on(CoreEvents.showModal, () => (this.modalOpen = true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGlobal() {
|
reset() {
|
||||||
if (!(this.$location.path() === '/login')) {
|
Mousetrap.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
initGlobals() {
|
||||||
|
if (locationService.getLocation().pathname !== '/login') {
|
||||||
this.bind(['?', 'h'], this.showHelpModal);
|
this.bind(['?', 'h'], this.showHelpModal);
|
||||||
this.bind('g h', this.goToHome);
|
this.bind('g h', this.goToHome);
|
||||||
this.bind('g a', this.openAlerting);
|
this.bind('g a', this.openAlerting);
|
||||||
@ -53,7 +45,7 @@ export class KeybindingSrv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
globalEsc() {
|
private globalEsc() {
|
||||||
const anyDoc = document as any;
|
const anyDoc = document as any;
|
||||||
const activeElement = anyDoc.activeElement;
|
const activeElement = anyDoc.activeElement;
|
||||||
|
|
||||||
@ -79,68 +71,62 @@ export class KeybindingSrv {
|
|||||||
this.exit();
|
this.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
openSearch() {
|
private openSearch() {
|
||||||
const search = _.extend(this.$location.search(), { search: 'open' });
|
locationService.partial({ search: 'open' });
|
||||||
this.$location.search(search);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeSearch() {
|
private closeSearch() {
|
||||||
const search = _.extend(this.$location.search(), { search: null, ...defaultQueryParams });
|
locationService.partial({ search: null });
|
||||||
this.$location.search(search);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openAlerting() {
|
private openAlerting() {
|
||||||
this.$location.url('/alerting');
|
locationService.push('/alerting');
|
||||||
}
|
}
|
||||||
|
|
||||||
goToHome() {
|
private goToHome() {
|
||||||
this.$location.url('/');
|
locationService.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
goToProfile() {
|
private goToProfile() {
|
||||||
this.$location.url('/profile');
|
locationService.push('/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
showHelpModal() {
|
private showHelpModal() {
|
||||||
appEvents.emit(CoreEvents.showModal, { templateHtml: '<help-modal></help-modal>' });
|
appEvents.publish(new ShowModalEvent({ templateHtml: '<help-modal></help-modal>' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
exit() {
|
private exit() {
|
||||||
appEvents.emit(CoreEvents.hideModal);
|
appEvents.publish(new HideModalEvent());
|
||||||
|
|
||||||
if (this.modalOpen) {
|
if (this.modalOpen) {
|
||||||
this.modalOpen = false;
|
this.modalOpen = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// close settings view
|
const search = locationService.getSearchObject();
|
||||||
const search = this.$location.search();
|
|
||||||
if (search.editview) {
|
if (search.editview) {
|
||||||
delete search.editview;
|
locationService.partial({ editview: null });
|
||||||
this.$location.search(search);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.inspect) {
|
if (search.inspect) {
|
||||||
delete search.inspect;
|
locationService.partial({ inspect: null, inspectTab: null });
|
||||||
delete search.inspectTab;
|
|
||||||
this.$location.search(search);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.editPanel) {
|
if (search.editPanel) {
|
||||||
dispatch(exitPanelEditor());
|
locationService.partial({ editPanel: null, tab: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.viewPanel) {
|
if (search.viewPanel) {
|
||||||
delete search.viewPanel;
|
locationService.partial({ viewPanel: null, tab: null });
|
||||||
this.$location.search(search);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.kiosk) {
|
if (search.kiosk) {
|
||||||
this.$rootScope.appEvent(CoreEvents.toggleKioskMode, { exit: true });
|
exitKioskMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search.search) {
|
if (search.search) {
|
||||||
@ -148,6 +134,12 @@ export class KeybindingSrv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showDashEditView() {
|
||||||
|
locationService.partial({
|
||||||
|
editview: 'settings',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bind(keyArg: string | string[], fn: () => void) {
|
bind(keyArg: string | string[], fn: () => void) {
|
||||||
Mousetrap.bind(
|
Mousetrap.bind(
|
||||||
keyArg,
|
keyArg,
|
||||||
@ -155,7 +147,7 @@ export class KeybindingSrv {
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.returnValue = false;
|
evt.returnValue = false;
|
||||||
return this.$rootScope.$apply(fn.bind(this));
|
fn.call(this);
|
||||||
},
|
},
|
||||||
'keydown'
|
'keydown'
|
||||||
);
|
);
|
||||||
@ -168,7 +160,7 @@ export class KeybindingSrv {
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.returnValue = false;
|
evt.returnValue = false;
|
||||||
return this.$rootScope.$apply(fn.bind(this));
|
fn.call(this);
|
||||||
},
|
},
|
||||||
'keydown'
|
'keydown'
|
||||||
);
|
);
|
||||||
@ -178,12 +170,7 @@ export class KeybindingSrv {
|
|||||||
Mousetrap.unbind(keyArg, keyType);
|
Mousetrap.unbind(keyArg, keyType);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDashEditView() {
|
setupDashboardBindings(dashboard: DashboardModel) {
|
||||||
const search = _.extend(this.$location.search(), { editview: 'settings' });
|
|
||||||
this.$location.search(search);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDashboardBindings(scope: IRootScopeService & AppEventEmitter, dashboard: DashboardModel) {
|
|
||||||
this.bind('mod+o', () => {
|
this.bind('mod+o', () => {
|
||||||
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
||||||
dashboard.events.publish(new LegacyGraphHoverClearEvent());
|
dashboard.events.publish(new LegacyGraphHoverClearEvent());
|
||||||
@ -191,28 +178,30 @@ export class KeybindingSrv {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.bind('mod+s', () => {
|
this.bind('mod+s', () => {
|
||||||
appEvents.emit(CoreEvents.showModalReact, {
|
appEvents.publish(
|
||||||
|
new ShowModalReactEvent({
|
||||||
component: SaveDashboardModalProxy,
|
component: SaveDashboardModalProxy,
|
||||||
props: {
|
props: {
|
||||||
dashboard,
|
dashboard,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('t z', () => {
|
this.bind('t z', () => {
|
||||||
scope.appEvent(CoreEvents.zoomOut, 2);
|
appEvents.publish(new ZoomOutEvent(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('ctrl+z', () => {
|
this.bind('ctrl+z', () => {
|
||||||
scope.appEvent(CoreEvents.zoomOut, 2);
|
appEvents.publish(new ZoomOutEvent(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('t left', () => {
|
this.bind('t left', () => {
|
||||||
scope.appEvent(CoreEvents.shiftTime, -1);
|
appEvents.publish(new ShiftTimeEvent(ShiftTimeEventPayload.Left));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('t right', () => {
|
this.bind('t right', () => {
|
||||||
scope.appEvent(CoreEvents.shiftTime, 1);
|
appEvents.publish(new ShiftTimeEvent(ShiftTimeEventPayload.Right));
|
||||||
});
|
});
|
||||||
|
|
||||||
// edit panel
|
// edit panel
|
||||||
@ -222,44 +211,47 @@ export class KeybindingSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dashboard.canEditPanelById(dashboard.meta.focusPanelId)) {
|
if (dashboard.canEditPanelById(dashboard.meta.focusPanelId)) {
|
||||||
const search = _.extend(this.$location.search(), { editPanel: dashboard.meta.focusPanelId });
|
locationService.partial({
|
||||||
this.$location.search(search);
|
editPanel: dashboard.meta.focusPanelId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// view panel
|
// view panel
|
||||||
this.bind('v', () => {
|
this.bind('v', () => {
|
||||||
if (dashboard.meta.focusPanelId) {
|
if (dashboard.meta.focusPanelId) {
|
||||||
const search = _.extend(this.$location.search(), { viewPanel: dashboard.meta.focusPanelId });
|
locationService.partial({
|
||||||
this.$location.search(search);
|
viewPanel: dashboard.meta.focusPanelId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('i', () => {
|
this.bind('i', () => {
|
||||||
if (dashboard.meta.focusPanelId) {
|
if (dashboard.meta.focusPanelId) {
|
||||||
const search = _.extend(this.$location.search(), { inspect: dashboard.meta.focusPanelId });
|
locationService.partial({
|
||||||
this.$location.search(search);
|
inspect: dashboard.meta.focusPanelId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// jump to explore if permissions allow
|
// jump to explore if permissions allow
|
||||||
if (this.contextSrv.hasAccessToExplore()) {
|
if (contextSrv.hasAccessToExplore()) {
|
||||||
this.bind('x', async () => {
|
this.bind('x', async () => {
|
||||||
if (dashboard.meta.focusPanelId) {
|
if (dashboard.meta.focusPanelId) {
|
||||||
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId)!;
|
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId)!;
|
||||||
const datasource = await this.datasourceSrv.get(panel.datasource);
|
const datasource = await getDatasourceSrv().get(panel.datasource);
|
||||||
const url = await getExploreUrl({
|
const url = await getExploreUrl({
|
||||||
panel,
|
panel,
|
||||||
panelTargets: panel.targets,
|
panelTargets: panel.targets,
|
||||||
panelDatasource: datasource,
|
panelDatasource: datasource,
|
||||||
datasourceSrv: this.datasourceSrv,
|
datasourceSrv: getDatasourceSrv(),
|
||||||
timeSrv: this.timeSrv,
|
timeSrv: getTimeSrv(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
const urlWithoutBase = locationUtil.stripBaseFromUrl(url);
|
const urlWithoutBase = locationUtil.stripBaseFromUrl(url);
|
||||||
if (urlWithoutBase) {
|
if (urlWithoutBase) {
|
||||||
this.$timeout(() => this.$location.url(urlWithoutBase));
|
locationService.push(urlWithoutBase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -271,7 +263,7 @@ export class KeybindingSrv {
|
|||||||
const panelId = dashboard.meta.focusPanelId;
|
const panelId = dashboard.meta.focusPanelId;
|
||||||
|
|
||||||
if (panelId && dashboard.canEditPanelById(panelId) && !(dashboard.panelInView || dashboard.panelInEdit)) {
|
if (panelId && dashboard.canEditPanelById(panelId) && !(dashboard.panelInView || dashboard.panelInEdit)) {
|
||||||
appEvents.emit(CoreEvents.removePanel, panelId);
|
appEvents.publish(new RemovePanelEvent(panelId));
|
||||||
dashboard.meta.focusPanelId = 0;
|
dashboard.meta.focusPanelId = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -291,13 +283,15 @@ export class KeybindingSrv {
|
|||||||
if (dashboard.meta.focusPanelId) {
|
if (dashboard.meta.focusPanelId) {
|
||||||
const panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
|
const panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
|
||||||
|
|
||||||
appEvents.emit(CoreEvents.showModalReact, {
|
appEvents.publish(
|
||||||
|
new ShowModalReactEvent({
|
||||||
component: ShareModal,
|
component: ShareModal,
|
||||||
props: {
|
props: {
|
||||||
dashboard: dashboard,
|
dashboard: dashboard,
|
||||||
panel: panelInfo?.panel,
|
panel: panelInfo?.panel,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -329,7 +323,7 @@ export class KeybindingSrv {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d n', () => {
|
this.bind('d n', () => {
|
||||||
this.$location.url('/dashboard/new');
|
locationService.push('/dashboard/new');
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d r', () => {
|
this.bind('d r', () => {
|
||||||
@ -341,35 +335,17 @@ export class KeybindingSrv {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d k', () => {
|
this.bind('d k', () => {
|
||||||
appEvents.emit(CoreEvents.toggleKioskMode);
|
toggleKioskMode();
|
||||||
});
|
|
||||||
|
|
||||||
this.bind('d v', () => {
|
|
||||||
appEvents.emit(CoreEvents.toggleViewMode);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Autofit panels
|
//Autofit panels
|
||||||
this.bind('d a', () => {
|
this.bind('d a', () => {
|
||||||
// this has to be a full page reload
|
// this has to be a full page reload
|
||||||
const queryParams = store.getState().location.query;
|
const queryParams = locationService.getSearchObject();
|
||||||
const newUrlParam = queryParams.autofitpanels ? '' : '&autofitpanels';
|
const newUrlParam = queryParams.autofitpanels ? '' : '&autofitpanels';
|
||||||
window.location.href = window.location.href + newUrlParam;
|
window.location.href = window.location.href + newUrlParam;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreModule.service('keybindingSrv', KeybindingSrv);
|
export const keybindingSrv = new KeybindingSrv();
|
||||||
|
|
||||||
/**
|
|
||||||
* Code below exports the service to react components
|
|
||||||
*/
|
|
||||||
|
|
||||||
let singletonInstance: KeybindingSrv;
|
|
||||||
|
|
||||||
export function setKeybindingSrv(instance: KeybindingSrv) {
|
|
||||||
singletonInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getKeybindingSrv(): KeybindingSrv {
|
|
||||||
return singletonInstance;
|
|
||||||
}
|
|
||||||
|
@ -4,10 +4,11 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
import { AngularModalProxy } from '../components/modals/AngularModalProxy';
|
import { AngularModalProxy } from '../components/modals/AngularModalProxy';
|
||||||
import { provideTheme } from '../utils/ConfigProvider';
|
import { provideTheme } from '../utils/ConfigProvider';
|
||||||
|
import { HideModalEvent, ShowConfirmModalEvent, ShowModalEvent, ShowModalReactEvent } from '../../types/events';
|
||||||
|
|
||||||
export class UtilSrv {
|
export class UtilSrv {
|
||||||
modalScope: any;
|
modalScope: any;
|
||||||
@ -20,10 +21,10 @@ export class UtilSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
appEvents.on(CoreEvents.showModal, this.showModal.bind(this), this.$rootScope);
|
appEvents.subscribe(ShowModalEvent, (e) => this.showModal(e.payload));
|
||||||
appEvents.on(CoreEvents.hideModal, this.hideModal.bind(this), this.$rootScope);
|
appEvents.subscribe(HideModalEvent, this.hideModal.bind(this));
|
||||||
appEvents.on(CoreEvents.showConfirmModal, this.showConfirmModal.bind(this), this.$rootScope);
|
appEvents.subscribe(ShowConfirmModalEvent, (e) => this.showConfirmModal(e.payload));
|
||||||
appEvents.on(CoreEvents.showModalReact, this.showModalReact.bind(this), this.$rootScope);
|
appEvents.subscribe(ShowModalReactEvent, (e) => this.showModalReact(e.payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
showModalReact(options: any) {
|
showModalReact(options: any) {
|
||||||
@ -105,11 +106,13 @@ export class UtilSrv {
|
|||||||
scope.confirmTextValid = scope.confirmText ? false : true;
|
scope.confirmTextValid = scope.confirmText ? false : true;
|
||||||
scope.selectors = selectors.pages.ConfirmModal;
|
scope.selectors = selectors.pages.ConfirmModal;
|
||||||
|
|
||||||
appEvents.emit(CoreEvents.showModal, {
|
appEvents.publish(
|
||||||
|
new ShowModalEvent({
|
||||||
src: 'public/app/partials/confirm_modal.html',
|
src: 'public/app/partials/confirm_modal.html',
|
||||||
scope: scope,
|
scope: scope,
|
||||||
modalClass: 'confirm-modal',
|
modalClass: 'confirm-modal',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
// Check to see if browser is not supported by Grafana
|
/**
|
||||||
|
* Check to see if browser is not supported by Grafana
|
||||||
|
* This function is copied to index-template.html but is here so we can write tests
|
||||||
|
* */
|
||||||
export function checkBrowserCompatibility() {
|
export function checkBrowserCompatibility() {
|
||||||
const isIE = navigator.userAgent.indexOf('MSIE') > -1;
|
const isIE = navigator.userAgent.indexOf('MSIE') > -1;
|
||||||
const isEdge = navigator.userAgent.indexOf('Edge/') > -1 || navigator.userAgent.indexOf('Edg/') > -1;
|
const isEdge = navigator.userAgent.indexOf('Edge/') > -1 || navigator.userAgent.indexOf('Edg/') > -1;
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import { NavModelSrv } from 'app/core/core';
|
|
||||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
|
||||||
|
|
||||||
export default class AdminEditOrgCtrl {
|
|
||||||
/** @ngInject */
|
|
||||||
constructor($scope: any, $routeParams: any, $location: any, navModelSrv: NavModelSrv) {
|
|
||||||
$scope.init = () => {
|
|
||||||
$scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
|
|
||||||
|
|
||||||
if ($routeParams.id) {
|
|
||||||
promiseToDigest($scope)(Promise.all([$scope.getOrg($routeParams.id), $scope.getOrgUsers($routeParams.id)]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getOrg = (id: number) => {
|
|
||||||
return getBackendSrv()
|
|
||||||
.get('/api/orgs/' + id)
|
|
||||||
.then((org: any) => {
|
|
||||||
$scope.org = org;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getOrgUsers = (id: number) => {
|
|
||||||
return getBackendSrv()
|
|
||||||
.get('/api/orgs/' + id + '/users')
|
|
||||||
.then((orgUsers: any) => {
|
|
||||||
$scope.orgUsers = orgUsers;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.update = () => {
|
|
||||||
if (!$scope.orgDetailsForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
promiseToDigest($scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.put('/api/orgs/' + $scope.org.id, $scope.org)
|
|
||||||
.then(() => {
|
|
||||||
$location.path('/admin/orgs');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.updateOrgUser = (orgUser: any) => {
|
|
||||||
getBackendSrv().patch('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId, orgUser);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeOrgUser = (orgUser: any) => {
|
|
||||||
promiseToDigest($scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.delete('/api/orgs/' + orgUser.orgId + '/users/' + orgUser.userId)
|
|
||||||
.then(() => $scope.getOrgUsers($scope.org.id))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.init();
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
|||||||
import { UrlQueryValue } from '@grafana/data';
|
import { UrlQueryValue } from '@grafana/data';
|
||||||
import { Form, Field, Input, Button, Legend } from '@grafana/ui';
|
import { Form, Field, Input, Button, Legend } from '@grafana/ui';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
interface OrgNameDTO {
|
interface OrgNameDTO {
|
||||||
orgName: string;
|
orgName: string;
|
||||||
@ -30,11 +31,12 @@ const removeOrgUser = async (orgUser: OrgUser, orgId: UrlQueryValue) => {
|
|||||||
return await getBackendSrv().delete('/api/orgs/' + orgId + '/users/' + orgUser.userId);
|
return await getBackendSrv().delete('/api/orgs/' + orgId + '/users/' + orgUser.userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminEditOrgPage: FC = () => {
|
interface Props extends GrafanaRouteComponentProps<{ id: string }> {}
|
||||||
|
|
||||||
|
export const AdminEditOrgPage: FC<Props> = ({ match }) => {
|
||||||
const navIndex = useSelector((state: StoreState) => state.navIndex);
|
const navIndex = useSelector((state: StoreState) => state.navIndex);
|
||||||
const navModel = getNavModel(navIndex, 'global-orgs');
|
const navModel = getNavModel(navIndex, 'global-orgs');
|
||||||
|
const orgId = parseInt(match.params.id, 10);
|
||||||
const orgId = useSelector((state: StoreState) => state.location.routeParams.id);
|
|
||||||
|
|
||||||
const [users, setUsers] = useState<OrgUser[]>([]);
|
const [users, setUsers] = useState<OrgUser[]>([]);
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { hot } from 'react-hot-loader';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NavModel } from '@grafana/data';
|
import { NavModel } from '@grafana/data';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { UserProfile } from './UserProfile';
|
import { UserProfile } from './UserProfile';
|
||||||
@ -27,10 +26,10 @@ import {
|
|||||||
syncLdapUser,
|
syncLdapUser,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import { UserOrgs } from './UserOrgs';
|
import { UserOrgs } from './UserOrgs';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props extends GrafanaRouteComponentProps<{ id: string }> {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
userId: number;
|
|
||||||
user: UserDTO;
|
user: UserDTO;
|
||||||
orgs: UserOrg[];
|
orgs: UserOrg[];
|
||||||
sessions: UserSession[];
|
sessions: UserSession[];
|
||||||
@ -63,8 +62,8 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { userId, loadAdminUserPage } = this.props;
|
const { match, loadAdminUserPage } = this.props;
|
||||||
loadAdminUserPage(userId);
|
loadAdminUserPage(parseInt(match.params.id, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserUpdate = (user: UserDTO) => {
|
onUserUpdate = (user: UserDTO) => {
|
||||||
@ -72,8 +71,8 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onPasswordChange = (password: string) => {
|
onPasswordChange = (password: string) => {
|
||||||
const { userId, setUserPassword } = this.props;
|
const { user, setUserPassword } = this.props;
|
||||||
setUserPassword(userId, password);
|
setUserPassword(user.id, password);
|
||||||
};
|
};
|
||||||
|
|
||||||
onUserDelete = (userId: number) => {
|
onUserDelete = (userId: number) => {
|
||||||
@ -89,18 +88,18 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onGrafanaAdminChange = (isGrafanaAdmin: boolean) => {
|
onGrafanaAdminChange = (isGrafanaAdmin: boolean) => {
|
||||||
const { userId, updateUserPermissions } = this.props;
|
const { user, updateUserPermissions } = this.props;
|
||||||
updateUserPermissions(userId, isGrafanaAdmin);
|
updateUserPermissions(user.id, isGrafanaAdmin);
|
||||||
};
|
};
|
||||||
|
|
||||||
onOrgRemove = (orgId: number) => {
|
onOrgRemove = (orgId: number) => {
|
||||||
const { userId, deleteOrgUser } = this.props;
|
const { user, deleteOrgUser } = this.props;
|
||||||
deleteOrgUser(userId, orgId);
|
deleteOrgUser(user.id, orgId);
|
||||||
};
|
};
|
||||||
|
|
||||||
onOrgRoleChange = (orgId: number, newRole: string) => {
|
onOrgRoleChange = (orgId: number, newRole: string) => {
|
||||||
const { userId, updateOrgUserRole } = this.props;
|
const { user, updateOrgUserRole } = this.props;
|
||||||
updateOrgUserRole(userId, orgId, newRole);
|
updateOrgUserRole(user.id, orgId, newRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
onOrgAdd = (orgId: number, role: string) => {
|
onOrgAdd = (orgId: number, role: string) => {
|
||||||
@ -109,18 +108,18 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSessionRevoke = (tokenId: number) => {
|
onSessionRevoke = (tokenId: number) => {
|
||||||
const { userId, revokeSession } = this.props;
|
const { user, revokeSession } = this.props;
|
||||||
revokeSession(tokenId, userId);
|
revokeSession(tokenId, user.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
onAllSessionsRevoke = () => {
|
onAllSessionsRevoke = () => {
|
||||||
const { userId, revokeAllSessions } = this.props;
|
const { user, revokeAllSessions } = this.props;
|
||||||
revokeAllSessions(userId);
|
revokeAllSessions(user.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
onUserSync = () => {
|
onUserSync = () => {
|
||||||
const { userId, syncLdapUser } = this.props;
|
const { user, syncLdapUser } = this.props;
|
||||||
syncLdapUser(userId);
|
syncLdapUser(user.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -171,7 +170,6 @@ export class UserAdminPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
userId: getRouteParamsId(state.location),
|
|
||||||
navModel: getNavModel(state.navIndex, 'global-users'),
|
navModel: getNavModel(state.navIndex, 'global-users'),
|
||||||
user: state.userAdmin.user,
|
user: state.userAdmin.user,
|
||||||
sessions: state.userAdmin.sessions,
|
sessions: state.userAdmin.sessions,
|
||||||
|
@ -7,11 +7,10 @@ import { getBackendSrv } from '@grafana/runtime';
|
|||||||
import { StoreState } from '../../types';
|
import { StoreState } from '../../types';
|
||||||
import { getNavModel } from '../../core/selectors/navModel';
|
import { getNavModel } from '../../core/selectors/navModel';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { updateLocation } from 'app/core/actions';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
interface UserCreatePageProps {
|
interface UserCreatePageProps {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
updateLocation: typeof updateLocation;
|
|
||||||
}
|
}
|
||||||
interface UserDTO {
|
interface UserDTO {
|
||||||
name: string;
|
name: string;
|
||||||
@ -22,10 +21,12 @@ interface UserDTO {
|
|||||||
|
|
||||||
const createUser = async (user: UserDTO) => getBackendSrv().post('/api/admin/users', user);
|
const createUser = async (user: UserDTO) => getBackendSrv().post('/api/admin/users', user);
|
||||||
|
|
||||||
const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel, updateLocation }) => {
|
const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const onSubmit = useCallback(async (data: UserDTO) => {
|
const onSubmit = useCallback(async (data: UserDTO) => {
|
||||||
await createUser(data);
|
await createUser(data);
|
||||||
updateLocation({ path: '/admin/users' });
|
history.push('/admin/users');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,7 +81,4 @@ const mapStateToProps = (state: StoreState) => ({
|
|||||||
navModel: getNavModel(state.navIndex, 'global-users'),
|
navModel: getNavModel(state.navIndex, 'global-users'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
export default hot(module)(connect(mapStateToProps)(UserCreatePage));
|
||||||
updateLocation,
|
|
||||||
};
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UserCreatePage));
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import AdminEditOrgCtrl from './AdminEditOrgCtrl';
|
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
import { NavModelSrv } from 'app/core/core';
|
|
||||||
|
|
||||||
class AdminHomeCtrl {
|
|
||||||
navModel: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(navModelSrv: NavModelSrv) {
|
|
||||||
this.navModel = navModelSrv.getNav('admin');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('AdminEditOrgCtrl', AdminEditOrgCtrl);
|
|
||||||
coreModule.controller('AdminHomeCtrl', AdminHomeCtrl);
|
|
@ -18,15 +18,15 @@ import {
|
|||||||
clearUserError,
|
clearUserError,
|
||||||
clearUserMappingInfo,
|
clearUserMappingInfo,
|
||||||
} from '../state/actions';
|
} from '../state/actions';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props extends GrafanaRouteComponentProps<{}, { username: string }> {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
ldapConnectionInfo: LdapConnectionInfo;
|
ldapConnectionInfo: LdapConnectionInfo;
|
||||||
ldapUser: LdapUser;
|
ldapUser: LdapUser;
|
||||||
ldapSyncInfo: SyncInfo;
|
ldapSyncInfo: SyncInfo;
|
||||||
ldapError: LdapError;
|
ldapError: LdapError;
|
||||||
userError?: LdapError;
|
userError?: LdapError;
|
||||||
username?: string;
|
|
||||||
|
|
||||||
loadLdapState: typeof loadLdapState;
|
loadLdapState: typeof loadLdapState;
|
||||||
loadLdapSyncStatus: typeof loadLdapSyncStatus;
|
loadLdapSyncStatus: typeof loadLdapSyncStatus;
|
||||||
@ -45,12 +45,14 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { username, clearUserMappingInfo, loadUserMapping } = this.props;
|
const { clearUserMappingInfo, queryParams } = this.props;
|
||||||
await clearUserMappingInfo();
|
await clearUserMappingInfo();
|
||||||
await this.fetchLDAPStatus();
|
await this.fetchLDAPStatus();
|
||||||
if (username) {
|
|
||||||
await loadUserMapping(username);
|
if (queryParams.username) {
|
||||||
|
await this.fetchUserMapping(queryParams.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ isLoading: false });
|
this.setState({ isLoading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +79,7 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ldapUser, userError, ldapError, ldapSyncInfo, ldapConnectionInfo, navModel, username } = this.props;
|
const { ldapUser, userError, ldapError, ldapSyncInfo, ldapConnectionInfo, navModel, queryParams } = this.props;
|
||||||
const { isLoading } = this.state;
|
const { isLoading } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -106,7 +108,7 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
defaultValue={username}
|
defaultValue={queryParams.username}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="btn btn-primary">
|
<button type="submit" className="btn btn-primary">
|
||||||
Run
|
Run
|
||||||
@ -134,7 +136,6 @@ export class LdapPage extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
const mapStateToProps = (state: StoreState) => ({
|
const mapStateToProps = (state: StoreState) => ({
|
||||||
navModel: getNavModel(state.navIndex, 'ldap'),
|
navModel: getNavModel(state.navIndex, 'ldap'),
|
||||||
username: state.location.routeParams.user,
|
|
||||||
ldapConnectionInfo: state.ldap.connectionInfo,
|
ldapConnectionInfo: state.ldap.connectionInfo,
|
||||||
ldapUser: state.ldap.user,
|
ldapUser: state.ldap.user,
|
||||||
ldapSyncInfo: state.ldap.syncInfo,
|
ldapSyncInfo: state.ldap.syncInfo,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
import { ThunkResult, LdapUser, UserSession, UserDTO } from 'app/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -74,8 +73,7 @@ export function setUserPassword(userId: number, password: string): ThunkResult<v
|
|||||||
export function disableUser(userId: number): ThunkResult<void> {
|
export function disableUser(userId: number): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
await getBackendSrv().post(`/api/admin/users/${userId}/disable`);
|
await getBackendSrv().post(`/api/admin/users/${userId}/disable`);
|
||||||
// dispatch(loadAdminUserPage(userId));
|
locationService.push('/admin/users');
|
||||||
dispatch(updateLocation({ path: '/admin/users' }));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +87,7 @@ export function enableUser(userId: number): ThunkResult<void> {
|
|||||||
export function deleteUser(userId: number): ThunkResult<void> {
|
export function deleteUser(userId: number): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
await getBackendSrv().delete(`/api/admin/users/${userId}`);
|
await getBackendSrv().delete(`/api/admin/users/${userId}`);
|
||||||
dispatch(updateLocation({ path: '/admin/users' }));
|
locationService.push('/admin/users');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,24 +4,24 @@ import { AlertRuleListUnconnected, Props } from './AlertRuleList';
|
|||||||
import { AlertRule } from '../../types';
|
import { AlertRule } from '../../types';
|
||||||
import appEvents from '../../core/app_events';
|
import appEvents from '../../core/app_events';
|
||||||
import { NavModel } from '@grafana/data';
|
import { NavModel } from '@grafana/data';
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
import { updateLocation } from '../../core/actions';
|
|
||||||
import { setSearchQuery } from './state/reducers';
|
import { setSearchQuery } from './state/reducers';
|
||||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||||
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import { ShowModalEvent } from '../../types/events';
|
||||||
|
|
||||||
jest.mock('../../core/app_events', () => ({
|
jest.mock('../../core/app_events', () => ({
|
||||||
emit: jest.fn(),
|
publish: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
|
...getRouteComponentProps({}),
|
||||||
navModel: {} as NavModel,
|
navModel: {} as NavModel,
|
||||||
alertRules: [] as AlertRule[],
|
alertRules: [] as AlertRule[],
|
||||||
updateLocation: mockToolkitActionCreator(updateLocation),
|
|
||||||
getAlertRulesAsync: jest.fn(),
|
getAlertRulesAsync: jest.fn(),
|
||||||
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
||||||
togglePauseAlertRule: jest.fn(),
|
togglePauseAlertRule: jest.fn(),
|
||||||
stateFilter: '',
|
|
||||||
search: '',
|
search: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
ngAlertDefinitions: [],
|
ngAlertDefinitions: [],
|
||||||
@ -52,7 +52,7 @@ describe('Life cycle', () => {
|
|||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
instance.fetchRules = jest.fn();
|
instance.fetchRules = jest.fn();
|
||||||
|
|
||||||
instance.componentDidUpdate({ stateFilter: 'ok' } as Props);
|
instance.componentDidUpdate({ queryParams: { state: 'ok' } } as any);
|
||||||
|
|
||||||
expect(instance.fetchRules).toHaveBeenCalled();
|
expect(instance.fetchRules).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -63,19 +63,16 @@ describe('Functions', () => {
|
|||||||
describe('Get state filter', () => {
|
describe('Get state filter', () => {
|
||||||
it('should get all if prop is not set', () => {
|
it('should get all if prop is not set', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
const stateFilter = instance.getStateFilter();
|
const stateFilter = instance.getStateFilter();
|
||||||
|
|
||||||
expect(stateFilter).toEqual('all');
|
expect(stateFilter).toEqual('all');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return state filter if set', () => {
|
it('should return state filter if set', () => {
|
||||||
const { instance } = setup({
|
const { instance } = setup({
|
||||||
stateFilter: 'ok',
|
queryParams: { state: 'ok' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const stateFilter = instance.getStateFilter();
|
const stateFilter = instance.getStateFilter();
|
||||||
|
|
||||||
expect(stateFilter).toEqual('ok');
|
expect(stateFilter).toEqual('ok');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -84,10 +81,8 @@ describe('Functions', () => {
|
|||||||
it('should update location', () => {
|
it('should update location', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
const mockEvent = { value: 'alerting' };
|
const mockEvent = { value: 'alerting' };
|
||||||
|
|
||||||
instance.onStateFilterChanged(mockEvent);
|
instance.onStateFilterChanged(mockEvent);
|
||||||
|
expect(locationService.getSearchObject().state).toBe('alerting');
|
||||||
expect(instance.props.updateLocation).toHaveBeenCalledWith({ query: { state: 'alerting' } });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,20 +92,20 @@ describe('Functions', () => {
|
|||||||
|
|
||||||
instance.onOpenHowTo();
|
instance.onOpenHowTo();
|
||||||
|
|
||||||
expect(appEvents.emit).toHaveBeenCalledWith(CoreEvents.showModal, {
|
expect(appEvents.publish).toHaveBeenCalledWith(
|
||||||
|
new ShowModalEvent({
|
||||||
src: 'public/app/features/alerting/partials/alert_howto.html',
|
src: 'public/app/features/alerting/partials/alert_howto.html',
|
||||||
modalClass: 'confirm-modal',
|
modalClass: 'confirm-modal',
|
||||||
model: {},
|
model: {},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Search query change', () => {
|
describe('Search query change', () => {
|
||||||
it('should set search query', () => {
|
it('should set search query', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
instance.onSearchQueryChange('dashboard');
|
instance.onSearchQueryChange('dashboard');
|
||||||
|
|
||||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
|
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,23 +4,23 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import AlertRuleItem from './AlertRuleItem';
|
import AlertRuleItem from './AlertRuleItem';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { AlertDefinition, AlertRule, CoreEvents, StoreState } from 'app/types';
|
import { AlertDefinition, AlertRule, StoreState } from 'app/types';
|
||||||
import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions';
|
import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions';
|
||||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import { setSearchQuery } from './state/reducers';
|
import { setSearchQuery } from './state/reducers';
|
||||||
import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui';
|
import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui';
|
||||||
import { AlertDefinitionItem } from './components/AlertDefinitionItem';
|
import { AlertDefinitionItem } from './components/AlertDefinitionItem';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
import { ShowModalEvent } from '../../types/events';
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState) {
|
function mapStateToProps(state: StoreState) {
|
||||||
return {
|
return {
|
||||||
navModel: getNavModel(state.navIndex, 'alert-list'),
|
navModel: getNavModel(state.navIndex, 'alert-list'),
|
||||||
alertRules: getAlertRuleItems(state),
|
alertRules: getAlertRuleItems(state),
|
||||||
stateFilter: state.location.query.state,
|
|
||||||
search: getSearchQuery(state.alertRules),
|
search: getSearchQuery(state.alertRules),
|
||||||
isLoading: state.alertRules.isLoading,
|
isLoading: state.alertRules.isLoading,
|
||||||
ngAlertDefinitions: state.alertDefinition.alertDefinitions,
|
ngAlertDefinitions: state.alertDefinition.alertDefinitions,
|
||||||
@ -28,7 +28,6 @@ function mapStateToProps(state: StoreState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
updateLocation,
|
|
||||||
getAlertRulesAsync,
|
getAlertRulesAsync,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
togglePauseAlertRule,
|
togglePauseAlertRule,
|
||||||
@ -36,11 +35,11 @@ const mapDispatchToProps = {
|
|||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
interface OwnProps {}
|
interface OwnProps extends GrafanaRouteComponentProps<{}, { state: string }> {}
|
||||||
|
|
||||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
export class AlertRuleListUnconnected extends PureComponent<Props, any> {
|
export class AlertRuleListUnconnected extends PureComponent<Props> {
|
||||||
stateFilters = [
|
stateFilters = [
|
||||||
{ label: 'All', value: 'all' },
|
{ label: 'All', value: 'all' },
|
||||||
{ label: 'OK', value: 'ok' },
|
{ label: 'OK', value: 'ok' },
|
||||||
@ -56,7 +55,7 @@ export class AlertRuleListUnconnected extends PureComponent<Props, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
if (prevProps.stateFilter !== this.props.stateFilter) {
|
if (prevProps.queryParams.state !== this.props.queryParams.state) {
|
||||||
this.fetchRules();
|
this.fetchRules();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,25 +65,21 @@ export class AlertRuleListUnconnected extends PureComponent<Props, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStateFilter(): string {
|
getStateFilter(): string {
|
||||||
const { stateFilter } = this.props;
|
return this.props.queryParams.state ?? 'all';
|
||||||
if (stateFilter) {
|
|
||||||
return stateFilter.toString();
|
|
||||||
}
|
|
||||||
return 'all';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStateFilterChanged = (option: SelectableValue) => {
|
onStateFilterChanged = (option: SelectableValue) => {
|
||||||
this.props.updateLocation({
|
locationService.partial({ state: option.value });
|
||||||
query: { state: option.value },
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onOpenHowTo = () => {
|
onOpenHowTo = () => {
|
||||||
appEvents.emit(CoreEvents.showModal, {
|
appEvents.publish(
|
||||||
|
new ShowModalEvent({
|
||||||
src: 'public/app/features/alerting/partials/alert_howto.html',
|
src: 'public/app/features/alerting/partials/alert_howto.html',
|
||||||
modalClass: 'confirm-modal',
|
modalClass: 'confirm-modal',
|
||||||
model: {},
|
model: {},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchQueryChange = (value: string) => {
|
onSearchQueryChange = (value: string) => {
|
||||||
|
@ -13,6 +13,7 @@ import { PanelModel } from 'app/features/dashboard/state';
|
|||||||
import { getDefaultCondition } from './getAlertingValidationMessage';
|
import { getDefaultCondition } from './getAlertingValidationMessage';
|
||||||
import { CoreEvents } from 'app/types';
|
import { CoreEvents } from 'app/types';
|
||||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
||||||
|
import { ShowConfirmModalEvent } from '../../types/events';
|
||||||
|
|
||||||
export class AlertTabCtrl {
|
export class AlertTabCtrl {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -230,7 +231,8 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
appEvents.publish(
|
||||||
|
new ShowConfirmModalEvent({
|
||||||
title: 'Notifier with invalid identifier is detected',
|
title: 'Notifier with invalid identifier is detected',
|
||||||
text: `Do you want to delete notifier with invalid identifier: ${identifier} from the dashboard JSON?`,
|
text: `Do you want to delete notifier with invalid identifier: ${identifier} from the dashboard JSON?`,
|
||||||
text2: 'After successful deletion, make sure to save the dashboard for storing the update JSON.',
|
text2: 'After successful deletion, make sure to save the dashboard for storing the update JSON.',
|
||||||
@ -240,7 +242,8 @@ export class AlertTabCtrl {
|
|||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
this.removeNotification(addedNotification);
|
this.removeNotification(addedNotification);
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model && model.isDefault === false) {
|
if (model && model.isDefault === false) {
|
||||||
@ -424,7 +427,8 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
appEvents.publish(
|
||||||
|
new ShowConfirmModalEvent({
|
||||||
title: 'Delete Alert',
|
title: 'Delete Alert',
|
||||||
text: 'Are you sure you want to delete this alert rule?',
|
text: 'Are you sure you want to delete this alert rule?',
|
||||||
text2: 'You need to save dashboard for the delete to take effect',
|
text2: 'You need to save dashboard for the delete to take effect',
|
||||||
@ -438,7 +442,8 @@ export class AlertTabCtrl {
|
|||||||
this.panelCtrl.alertState = null;
|
this.panelCtrl.alertState = null;
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enable = () => {
|
enable = () => {
|
||||||
@ -474,7 +479,8 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearHistory() {
|
clearHistory() {
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
appEvents.publish(
|
||||||
|
new ShowConfirmModalEvent({
|
||||||
title: 'Delete Alert History',
|
title: 'Delete Alert History',
|
||||||
text: 'Are you sure you want to remove all history & annotations for this alert?',
|
text: 'Are you sure you want to remove all history & annotations for this alert?',
|
||||||
icon: 'trash-alt',
|
icon: 'trash-alt',
|
||||||
@ -492,7 +498,8 @@ export class AlertTabCtrl {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,16 +8,15 @@ import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
|
|||||||
import { NotificationChannelForm } from './components/NotificationChannelForm';
|
import { NotificationChannelForm } from './components/NotificationChannelForm';
|
||||||
import { loadNotificationChannel, testNotificationChannel, updateNotificationChannel } from './state/actions';
|
import { loadNotificationChannel, testNotificationChannel, updateNotificationChannel } from './state/actions';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
|
||||||
import { mapChannelsToSelectableValue, transformSubmitData, transformTestData } from './utils/notificationChannels';
|
import { mapChannelsToSelectableValue, transformSubmitData, transformTestData } from './utils/notificationChannels';
|
||||||
import { NotificationChannelType, NotificationChannelDTO, StoreState } from 'app/types';
|
import { NotificationChannelType, NotificationChannelDTO, StoreState } from 'app/types';
|
||||||
import { resetSecureField } from './state/reducers';
|
import { resetSecureField } from './state/reducers';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
interface OwnProps {}
|
interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {}
|
||||||
|
|
||||||
interface ConnectedProps {
|
interface ConnectedProps {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
channelId: number;
|
|
||||||
notificationChannel: any;
|
notificationChannel: any;
|
||||||
notificationChannelTypes: NotificationChannelType[];
|
notificationChannelTypes: NotificationChannelType[];
|
||||||
}
|
}
|
||||||
@ -33,9 +32,7 @@ type Props = OwnProps & ConnectedProps & DispatchProps;
|
|||||||
|
|
||||||
export class EditNotificationChannelPage extends PureComponent<Props> {
|
export class EditNotificationChannelPage extends PureComponent<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { channelId } = this.props;
|
this.props.loadNotificationChannel(parseInt(this.props.match.params.id, 10));
|
||||||
|
|
||||||
this.props.loadNotificationChannel(channelId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = (formData: NotificationChannelDTO) => {
|
onSubmit = (formData: NotificationChannelDTO) => {
|
||||||
@ -119,10 +116,8 @@ export class EditNotificationChannelPage extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
|
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
|
||||||
const channelId = getRouteParamsId(state.location) as number;
|
|
||||||
return {
|
return {
|
||||||
navModel: getNavModel(state.navIndex, 'channels'),
|
navModel: getNavModel(state.navIndex, 'channels'),
|
||||||
channelId,
|
|
||||||
notificationChannel: state.notificationChannel.notificationChannel,
|
notificationChannel: state.notificationChannel.notificationChannel,
|
||||||
notificationChannelTypes: state.notificationChannel.notificationChannelTypes,
|
notificationChannelTypes: state.notificationChannel.notificationChannelTypes,
|
||||||
};
|
};
|
||||||
|
@ -21,19 +21,17 @@ import {
|
|||||||
updateAlertDefinitionOption,
|
updateAlertDefinitionOption,
|
||||||
updateAlertDefinitionUiState,
|
updateAlertDefinitionUiState,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
function mapStateToProps(state: StoreState) {
|
function mapStateToProps(state: StoreState, props: RouteProps) {
|
||||||
const pageId = getRouteParamsId(state.location);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uiState: state.alertDefinition.uiState,
|
uiState: state.alertDefinition.uiState,
|
||||||
getQueryOptions: state.alertDefinition.getQueryOptions,
|
getQueryOptions: state.alertDefinition.getQueryOptions,
|
||||||
queryRunner: state.alertDefinition.queryRunner,
|
queryRunner: state.alertDefinition.queryRunner,
|
||||||
getInstances: state.alertDefinition.getInstances,
|
getInstances: state.alertDefinition.getInstances,
|
||||||
alertDefinition: state.alertDefinition.alertDefinition,
|
alertDefinition: state.alertDefinition.alertDefinition,
|
||||||
pageId: (pageId as string) ?? '',
|
pageId: props.match.params.id as string,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +49,8 @@ const mapDispatchToProps = {
|
|||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
interface RouteProps extends GrafanaRouteComponentProps<{ id: string }> {}
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
|
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
|
||||||
}
|
}
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { appEvents, coreModule, NavModelSrv } from 'app/core/core';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import { AppEvents } from '@grafana/data';
|
|
||||||
import { IScope } from 'angular';
|
|
||||||
import { promiseToDigest } from '../../core/utils/promiseToDigest';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
|
|
||||||
export class AlertNotificationEditCtrl {
|
|
||||||
theForm: any;
|
|
||||||
navModel: any;
|
|
||||||
testSeverity = 'critical';
|
|
||||||
notifiers: any;
|
|
||||||
notifierTemplateId: string;
|
|
||||||
isNew: boolean;
|
|
||||||
model: any;
|
|
||||||
defaults: any = {
|
|
||||||
type: 'email',
|
|
||||||
sendReminder: false,
|
|
||||||
disableResolveMessage: false,
|
|
||||||
frequency: '15m',
|
|
||||||
settings: {
|
|
||||||
httpMethod: 'POST',
|
|
||||||
autoResolve: true,
|
|
||||||
severity: 'critical',
|
|
||||||
uploadImage: true,
|
|
||||||
},
|
|
||||||
secureSettings: {},
|
|
||||||
isDefault: false,
|
|
||||||
};
|
|
||||||
getFrequencySuggestion: any;
|
|
||||||
rendererAvailable: boolean;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(
|
|
||||||
private $scope: IScope,
|
|
||||||
private $routeParams: any,
|
|
||||||
private $location: any,
|
|
||||||
private $templateCache: any,
|
|
||||||
navModelSrv: NavModelSrv
|
|
||||||
) {
|
|
||||||
this.navModel = navModelSrv.getNav('alerting', 'channels', 0);
|
|
||||||
this.isNew = !this.$routeParams.id;
|
|
||||||
|
|
||||||
this.getFrequencySuggestion = () => {
|
|
||||||
return ['1m', '5m', '10m', '15m', '30m', '1h'];
|
|
||||||
};
|
|
||||||
|
|
||||||
this.defaults.settings.uploadImage = config.rendererAvailable;
|
|
||||||
this.rendererAvailable = config.rendererAvailable;
|
|
||||||
|
|
||||||
promiseToDigest(this.$scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.get(`/api/alert-notifiers`)
|
|
||||||
.then((notifiers: any) => {
|
|
||||||
this.notifiers = notifiers;
|
|
||||||
|
|
||||||
// add option templates
|
|
||||||
for (const notifier of this.notifiers) {
|
|
||||||
this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.$routeParams.id) {
|
|
||||||
this.navModel.breadcrumbs.push({ text: 'New channel' });
|
|
||||||
this.navModel.node = { text: 'New channel' };
|
|
||||||
return _.defaults(this.model, this.defaults);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getBackendSrv()
|
|
||||||
.get(`/api/alert-notifications/${this.$routeParams.id}`)
|
|
||||||
.then((result: any) => {
|
|
||||||
this.navModel.breadcrumbs.push({ text: result.name });
|
|
||||||
this.navModel.node = { text: result.name };
|
|
||||||
result.settings = _.defaults(result.settings, this.defaults.settings);
|
|
||||||
result.secureSettings = _.defaults(result.secureSettings, this.defaults.secureSettings);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((model: any) => {
|
|
||||||
this.model = model;
|
|
||||||
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
|
||||||
if (!this.theForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.model.id) {
|
|
||||||
promiseToDigest(this.$scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.put(`/api/alert-notifications/${this.model.id}`, this.model)
|
|
||||||
.then((res: any) => {
|
|
||||||
this.model = res;
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Notification updated']);
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
if (err.data && err.data.error) {
|
|
||||||
appEvents.emit(AppEvents.alertError, [err.data.error]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
promiseToDigest(this.$scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.post(`/api/alert-notifications`, this.model)
|
|
||||||
.then((res: any) => {
|
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Notification created']);
|
|
||||||
this.$location.path('alerting/notifications');
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
if (err.data && err.data.error) {
|
|
||||||
appEvents.emit(AppEvents.alertError, [err.data.error]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteNotification() {
|
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
|
||||||
title: 'Delete',
|
|
||||||
text: 'Do you want to delete this notification channel?',
|
|
||||||
text2: `Deleting this notification channel will not delete from alerts any references to it`,
|
|
||||||
icon: 'trash-alt',
|
|
||||||
confirmText: 'Delete',
|
|
||||||
yesText: 'Delete',
|
|
||||||
onConfirm: () => {
|
|
||||||
this.deleteNotificationConfirmed();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteNotificationConfirmed() {
|
|
||||||
promiseToDigest(this.$scope)(
|
|
||||||
getBackendSrv()
|
|
||||||
.delete(`/api/alert-notifications/${this.model.id}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
this.model = res;
|
|
||||||
this.$location.path('alerting/notifications');
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getNotifierTemplateId(type: string) {
|
|
||||||
return `notifier-options-${type}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
typeChanged() {
|
|
||||||
this.model.settings = _.defaults({}, this.defaults.settings);
|
|
||||||
this.model.secureSettings = _.defaults({}, this.defaults.secureSettings);
|
|
||||||
this.notifierTemplateId = this.getNotifierTemplateId(this.model.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
testNotification() {
|
|
||||||
if (!this.theForm.$valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {
|
|
||||||
name: this.model.name,
|
|
||||||
type: this.model.type,
|
|
||||||
frequency: this.model.frequency,
|
|
||||||
settings: this.model.settings,
|
|
||||||
secureSettings: this.model.secureSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.model.id) {
|
|
||||||
payload.id = this.model.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
promiseToDigest(this.$scope)(getBackendSrv().post(`/api/alert-notifications/test`, payload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl);
|
|
@ -6,8 +6,8 @@ import { useAsyncFn } from 'react-use';
|
|||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||||
import { HorizontalGroup, Button, LinkButton } from '@grafana/ui';
|
import { HorizontalGroup, Button, LinkButton } from '@grafana/ui';
|
||||||
import { CoreEvents } from 'app/types';
|
|
||||||
import { AlertNotification } from 'app/types/alerting';
|
import { AlertNotification } from 'app/types/alerting';
|
||||||
|
import { ShowConfirmModalEvent } from '../../types/events';
|
||||||
|
|
||||||
const NotificationsListPage: FC = () => {
|
const NotificationsListPage: FC = () => {
|
||||||
const navModel = useNavModel('channels');
|
const navModel = useNavModel('channels');
|
||||||
@ -26,7 +26,8 @@ const NotificationsListPage: FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteNotification = (id: number) => {
|
const deleteNotification = (id: number) => {
|
||||||
appEvents.emit(CoreEvents.showConfirmModal, {
|
appEvents.publish(
|
||||||
|
new ShowConfirmModalEvent({
|
||||||
title: 'Delete',
|
title: 'Delete',
|
||||||
text: 'Do you want to delete this notification channel?',
|
text: 'Do you want to delete this notification channel?',
|
||||||
text2: `Deleting this notification channel will not delete from alerts any references to it`,
|
text2: `Deleting this notification channel will not delete from alerts any references to it`,
|
||||||
@ -36,7 +37,8 @@ const NotificationsListPage: FC = () => {
|
|||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
deleteNotificationConfirmed(id);
|
deleteNotificationConfirmed(id);
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteNotificationConfirmed = async (id: number) => {
|
const deleteNotificationConfirmed = async (id: number) => {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user