mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Migrate router and <app-view> to React (#4525)
* Migrate router and <app-view> to React: skeleton * Update layout on route change * Start moving page routes from angular to react * Move page routes to react except of public dashboard and visualization embed) * Move public dashboard and visualization embed routes to React * Replace $route/$routeParams usages * Some cleanup * Replace AngularJS $location service with implementation based on history library * Minor fix to how ApplicationView handles route change * Explicitly use global layout for each page instead of handling related stuff in ApplicationArea component * Error handling * Remove AngularJS and related dependencies * Move Parameter factory method to a separate file * Fix CSS (replace custom components with classes) * Fix: keep other url parts when updating location partially; refine code * Fix tests * Make router work in multi-org mode (respect <base> tag) * Optimzation: don't resolve route if path didn't change * Fix search input in header; error handling improvement (handle more errors in pages; global error handler for unhandled errors; dialog dismiss 'unhandled rejection' errors) * Fix page keys; fix navigateTo calls (third parameter not available) * Use relative links * Router: ignore location REPLACE events, resolve only on PUSH/POP * Fix tests * Remove unused jQuery reference * Show error from backend when creating Destination * Remove route.resolve where not necessary (used constant values) * New Query page: keep state on saving, reload when creating another new query * Use currentRoute.key instead of hard-coded keys for page components * Tidy up Router * Tidy up location service * Fix tests * Don't add parameters changes to browser's history * Fix test (improved fix) Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"angularjs-annotate",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-object-assign",
|
||||
["babel-plugin-transform-builtin-extend", {
|
||||
|
||||
@@ -29,12 +29,12 @@ body {
|
||||
font-family: @redash-font;
|
||||
position: relative;
|
||||
|
||||
app-view {
|
||||
#application-root {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
&.headless {
|
||||
app-view {
|
||||
#application-root {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@@ -45,11 +45,11 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
app-view {
|
||||
#application-root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
app-view,
|
||||
#application-root,
|
||||
#app-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -93,11 +93,10 @@ strong {
|
||||
@media (min-width: 768px) {
|
||||
.settings-screen,
|
||||
.home-page,
|
||||
page-dashboard-list,
|
||||
page-queries-list,
|
||||
page-alerts-list,
|
||||
alert-page,
|
||||
queries-search-results-page,
|
||||
.page-dashboard-list,
|
||||
.page-queries-list,
|
||||
.page-alerts-list,
|
||||
.alert-page,
|
||||
.fixed-container {
|
||||
.container {
|
||||
width: 750px;
|
||||
@@ -108,11 +107,10 @@ strong {
|
||||
@media (min-width: 992px) {
|
||||
.settings-screen,
|
||||
.home-page,
|
||||
page-dashboard-list,
|
||||
page-queries-list,
|
||||
page-alerts-list,
|
||||
alert-page,
|
||||
queries-search-results-page,
|
||||
.page-dashboard-list,
|
||||
.page-queries-list,
|
||||
.page-alerts-list,
|
||||
.alert-page,
|
||||
.fixed-container {
|
||||
.container {
|
||||
width: 970px;
|
||||
@@ -123,11 +121,10 @@ strong {
|
||||
@media (min-width: 1200px) {
|
||||
.settings-screen,
|
||||
.home-page,
|
||||
page-dashboard-list,
|
||||
page-queries-list,
|
||||
page-alerts-list,
|
||||
alert-page,
|
||||
queries-search-results-page,
|
||||
.page-dashboard-list,
|
||||
.page-queries-list,
|
||||
.page-alerts-list,
|
||||
.alert-page,
|
||||
.fixed-container {
|
||||
.container {
|
||||
width: 1170px;
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// hide indicator when app-view has content
|
||||
app-view:not(:empty) ~ .loading-indicator {
|
||||
// hide indicator when application has content
|
||||
#application-root:not(:empty) ~ .loading-indicator {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
pointer-events: none;
|
||||
@@ -48,4 +48,4 @@ app-view:not(:empty) ~ .loading-indicator {
|
||||
* {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ body.fixed-layout {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
app-view {
|
||||
#application-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
@@ -692,9 +692,5 @@ nav .rg-bottom {
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
favorites-control {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import React, { useCallback, useRef } from "react";
|
||||
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Button from "antd/lib/button";
|
||||
@@ -9,25 +9,28 @@ import Menu from "antd/lib/menu";
|
||||
import Input from "antd/lib/input";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
import FavoritesDropdown from "./components/FavoritesDropdown";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
|
||||
import { currentUser, Auth, clientConfig } from "@/services/auth";
|
||||
import { $location, $route } from "@/services/ng";
|
||||
import { Dashboard } from "@/services/dashboard";
|
||||
import { Query } from "@/services/query";
|
||||
import frontendVersion from "@/version.json";
|
||||
import logoUrl from "@/assets/images/redash_icon_small.png";
|
||||
|
||||
import "./AppHeader.less";
|
||||
import FavoritesDropdown from "./FavoritesDropdown";
|
||||
import "./index.less";
|
||||
|
||||
function onSearch(q) {
|
||||
$location.path("/queries").search({ q });
|
||||
$route.reload();
|
||||
navigateTo(`queries?q=${encodeURIComponent(q)}`);
|
||||
}
|
||||
|
||||
function DesktopNavbar() {
|
||||
const showCreateDashboardDialog = useCallback(() => {
|
||||
CreateDashboardDialog.showModal().result.catch(() => {}); // ignore dismiss
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app-header" data-platform="desktop">
|
||||
<div>
|
||||
@@ -62,7 +65,7 @@ function DesktopNavbar() {
|
||||
)}
|
||||
{currentUser.hasPermission("create_dashboard") && (
|
||||
<Menu.Item key="new-dashboard">
|
||||
<a onMouseUp={() => CreateDashboardDialog.showModal()}>New Dashboard</a>
|
||||
<a onMouseUp={showCreateDashboardDialog}>New Dashboard</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission("list_alerts") && (
|
||||
@@ -249,7 +252,7 @@ function MobileNavbar() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppHeader() {
|
||||
export default function ApplicationHeader() {
|
||||
return (
|
||||
<nav className="app-header-wrapper">
|
||||
<DesktopNavbar />
|
||||
@@ -8,13 +8,13 @@ nav .app-header {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
background: white;
|
||||
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, .15);
|
||||
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
|
||||
|
||||
.darker {
|
||||
color: #333 !important;
|
||||
|
||||
&:hover {
|
||||
color: #2196F3 !important;
|
||||
color: #2196f3 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ nav .app-header {
|
||||
height: 50px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.ant-btn {
|
||||
font-weight: 500;
|
||||
|
||||
@@ -70,7 +70,7 @@ nav .app-header {
|
||||
top: 2px;
|
||||
|
||||
svg {
|
||||
transition: transform .2s cubic-bezier(.75,0,.25,1);
|
||||
transition: transform 0.2s cubic-bezier(0.75, 0, 0.25, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ nav .app-header {
|
||||
.menu-item-button {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
|
||||
.ant-menu-root {
|
||||
margin: 0 5px;
|
||||
}
|
||||
@@ -198,10 +198,10 @@ nav .app-header {
|
||||
|
||||
.ant-dropdown-menu-item .help-trigger {
|
||||
display: inline;
|
||||
color: #2196F3;
|
||||
color: #2196f3;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu.favorites-dropdown {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||
import { Auth } from "@/services/auth";
|
||||
import organizationStatus from "@/services/organizationStatus";
|
||||
import ApplicationHeader from "./ApplicationHeader";
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
|
||||
export default function AuthenticatedPageWrapper({ bodyClass, children }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(!!Auth.isAuthenticated());
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
Promise.all([Auth.requireSession(), organizationStatus.refresh()])
|
||||
.then(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(!!Auth.isAuthenticated());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (bodyClass) {
|
||||
document.body.classList.toggle(bodyClass, true);
|
||||
return () => {
|
||||
document.body.classList.toggle(bodyClass, false);
|
||||
};
|
||||
}
|
||||
}, [bodyClass]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ApplicationHeader />
|
||||
<ErrorBoundary renderError={error => <ErrorMessage error={error} showOriginalMessage={false} />}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticatedPageWrapper.propTypes = {
|
||||
bodyClass: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
AuthenticatedPageWrapper.defaultProps = {
|
||||
bodyClass: null,
|
||||
children: null,
|
||||
};
|
||||
40
client/app/components/ApplicationArea/ErrorMessage.jsx
Normal file
40
client/app/components/ApplicationArea/ErrorMessage.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default function ErrorMessage({ error, showOriginalMessage }) {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
||||
const message = showOriginalMessage
|
||||
? error.message
|
||||
: "It seems like we encountered an error. Try refreshing this page or contact your administrator.";
|
||||
|
||||
return (
|
||||
<div className="fixed-container" data-test="ErrorMessage">
|
||||
<div className="container">
|
||||
<div className="col-md-8 col-md-push-2">
|
||||
<div className="error-state bg-white tiled">
|
||||
<div className="error-state__icon">
|
||||
<i className="zmdi zmdi-alert-circle-o" />
|
||||
</div>
|
||||
<div className="error-state__details">
|
||||
<h4>{message}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
error: PropTypes.object.isRequired,
|
||||
showOriginalMessage: PropTypes.bool,
|
||||
};
|
||||
|
||||
ErrorMessage.defaultProps = {
|
||||
showOriginalMessage: true,
|
||||
};
|
||||
154
client/app/components/ApplicationArea/Router.jsx
Normal file
154
client/app/components/ApplicationArea/Router.jsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { isFunction, map, fromPairs, extend, startsWith, trimStart, trimEnd } from "lodash";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import UniversalRouter from "universal-router";
|
||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||
import location from "@/services/location";
|
||||
import url from "@/services/url";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
|
||||
function generateRouteKey() {
|
||||
return Math.random()
|
||||
.toString(32)
|
||||
.substr(2);
|
||||
}
|
||||
|
||||
export function stripBase(href) {
|
||||
// Resolve provided link and '' (root) relative to document's base.
|
||||
// If provided href is not related to current document (does not
|
||||
// start with resolved root) - return false. Otherwise
|
||||
// strip root and return relative url.
|
||||
|
||||
const baseHref = trimEnd(url.normalize(""), "/") + "/";
|
||||
href = url.normalize(href);
|
||||
|
||||
if (startsWith(href, baseHref)) {
|
||||
return "/" + trimStart(href.substr(baseHref.length), "/");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveRouteDependencies(route) {
|
||||
return Promise.all(
|
||||
map(route.resolve, (value, key) => {
|
||||
value = isFunction(value) ? value(route.routeParams, route, location) : value;
|
||||
return Promise.resolve(value).then(result => [key, result]);
|
||||
})
|
||||
).then(results => {
|
||||
route.routeParams = extend(route.routeParams, fromPairs(results));
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
export default function Router({ routes, onRouteChange }) {
|
||||
const [currentRoute, setCurrentRoute] = useState(null);
|
||||
|
||||
const currentPathRef = useRef(null);
|
||||
const errorHandlerRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
let isAbandoned = false;
|
||||
|
||||
const router = new UniversalRouter(routes, {
|
||||
resolveRoute({ route }, routeParams) {
|
||||
if (isFunction(route.render)) {
|
||||
return { ...route, routeParams };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function resolve(action) {
|
||||
if (!isAbandoned) {
|
||||
if (errorHandlerRef.current) {
|
||||
errorHandlerRef.current.reset();
|
||||
}
|
||||
|
||||
const pathname = stripBase(location.path);
|
||||
|
||||
// This is a optimization for route resolver: if current route was already resolved
|
||||
// from this path - do nothing. It also prevents router from using outdated route in a case
|
||||
// when user navigated to another path while current one was still resolving.
|
||||
// Note: this lock uses only `path` fragment of URL to distinguish routes because currently
|
||||
// all pages depend only on this fragment and handle search/hash on their own. If router
|
||||
// should reload page on search/hash change - this fragment (and few checks below) should be updated
|
||||
if (pathname === currentPathRef.current) {
|
||||
return;
|
||||
}
|
||||
currentPathRef.current = pathname;
|
||||
|
||||
// Don't reload controller if URL was replaced
|
||||
if (action === "REPLACE") {
|
||||
return;
|
||||
}
|
||||
|
||||
router
|
||||
.resolve({ pathname })
|
||||
.then(route => {
|
||||
return isAbandoned || currentPathRef.current !== pathname ? null : resolveRouteDependencies(route);
|
||||
})
|
||||
.then(route => {
|
||||
if (route) {
|
||||
setCurrentRoute({ ...route, key: generateRouteKey() });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (!isAbandoned && currentPathRef.current === pathname) {
|
||||
setCurrentRoute({
|
||||
render: params => <ErrorMessage {...params} />,
|
||||
routeParams: { error: new PromiseRejectionError(error) },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolve("PUSH");
|
||||
|
||||
const unlisten = location.listen((unused, action) => resolve(action));
|
||||
|
||||
return () => {
|
||||
isAbandoned = true;
|
||||
unlisten();
|
||||
};
|
||||
}, [routes]);
|
||||
|
||||
useEffect(() => {
|
||||
onRouteChange(currentRoute);
|
||||
}, [currentRoute, onRouteChange]);
|
||||
|
||||
if (!currentRoute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
ref={errorHandlerRef}
|
||||
renderError={error => <ErrorMessage error={error} showOriginalMessage={false} />}>
|
||||
{currentRoute.render(currentRoute)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
Router.propTypes = {
|
||||
routes: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
path: PropTypes.string.isRequired,
|
||||
render: PropTypes.func, // (routeParams: PropTypes.object; currentRoute; location) => PropTypes.node
|
||||
// Additional props to be injected into route component.
|
||||
// Object keys are props names. Object values will become prop values:
|
||||
// - if value is a function - it will be called without arguments, and result will be used; otherwise value will be used;
|
||||
// - after previous step, if value is a promise - router will wait for it to resolve; resolved value then will be used;
|
||||
// otherwise value will be used directly.
|
||||
resolve: PropTypes.objectOf(PropTypes.any),
|
||||
})
|
||||
),
|
||||
onRouteChange: PropTypes.func,
|
||||
};
|
||||
|
||||
Router.defaultProps = {
|
||||
routes: [],
|
||||
onRouteChange: () => {},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Auth } from "@/services/auth";
|
||||
|
||||
export default function SignedOutPageWrapper({ apiKey, children }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
Auth.setApiKey(apiKey);
|
||||
Auth.loadConfig()
|
||||
.then(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [apiKey]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
SignedOutPageWrapper.propTypes = {
|
||||
apiKey: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
SignedOutPageWrapper.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { isString } from "lodash";
|
||||
import navigateTo from "./navigateTo";
|
||||
|
||||
export default function handleNavigationIntent(event) {
|
||||
let element = event.target;
|
||||
while (element) {
|
||||
if (element.tagName === "A") {
|
||||
break;
|
||||
}
|
||||
element = element.parentNode;
|
||||
}
|
||||
if (!element || !element.hasAttribute("href")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep some default behaviour
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = element.getAttribute("target");
|
||||
if (isString(target) && target.toLowerCase() === "_blank") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
navigateTo(element.href);
|
||||
}
|
||||
37
client/app/components/ApplicationArea/index.jsx
Normal file
37
client/app/components/ApplicationArea/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import routes from "@/pages";
|
||||
import Router from "./Router";
|
||||
import handleNavigationIntent from "./handleNavigationIntent";
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
|
||||
export default function ApplicationArea() {
|
||||
const [currentRoute, setCurrentRoute] = useState(null);
|
||||
const [unhandledError, setUnhandledError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRoute && currentRoute.title) {
|
||||
document.title = currentRoute.title;
|
||||
}
|
||||
}, [currentRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
function globalErrorHandler(event) {
|
||||
event.preventDefault();
|
||||
setUnhandledError(event.error);
|
||||
}
|
||||
|
||||
document.body.addEventListener("click", handleNavigationIntent, false);
|
||||
window.addEventListener("error", globalErrorHandler, false);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener("click", handleNavigationIntent, false);
|
||||
window.removeEventListener("error", globalErrorHandler, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (unhandledError) {
|
||||
return <ErrorMessage error={unhandledError} showOriginalMessage={false} />;
|
||||
}
|
||||
|
||||
return <Router routes={routes} onRouteChange={setCurrentRoute} />;
|
||||
}
|
||||
25
client/app/components/ApplicationArea/navigateTo.js
Normal file
25
client/app/components/ApplicationArea/navigateTo.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import location from "@/services/location";
|
||||
import url from "@/services/url";
|
||||
import { stripBase } from "./Router";
|
||||
|
||||
// When `replace` is set to `true` - it will just replace current URL
|
||||
// without reloading current page (router will skip this location change)
|
||||
export default function navigateTo(href, replace = false) {
|
||||
// Allow calling chain to roll up, and then navigate
|
||||
setTimeout(() => {
|
||||
const isExternal = stripBase(href) === false;
|
||||
if (isExternal) {
|
||||
window.location = href;
|
||||
return;
|
||||
}
|
||||
href = url.parse(href);
|
||||
location.update(
|
||||
{
|
||||
path: href.pathname,
|
||||
search: href.search,
|
||||
hash: href.hash,
|
||||
},
|
||||
replace
|
||||
);
|
||||
}, 10);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { $rootScope } from "@/services/ng";
|
||||
|
||||
export default class FavoritesControl extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -21,7 +20,6 @@ export default class FavoritesControl extends React.Component {
|
||||
action().then(() => {
|
||||
item.is_favorite = !savedIsFavorite;
|
||||
this.forceUpdate();
|
||||
$rootScope.$broadcast("reloadFavorites");
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import Form from "antd/lib/form";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import ParameterValueInput from "@/components/ParameterValueInput";
|
||||
import { ParameterMappingType } from "@/services/widget";
|
||||
import { Parameter } from "@/services/parameters";
|
||||
import { Parameter, cloneParameter } from "@/services/parameters";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
import "./ParameterMappingInput.less";
|
||||
@@ -42,7 +42,7 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
|
||||
break;
|
||||
case ParameterMappingType.StaticValue:
|
||||
result.type = MappingType.StaticValue;
|
||||
result.param = result.param.clone();
|
||||
result.param = cloneParameter(result.param);
|
||||
result.param.setValue(result.value);
|
||||
break;
|
||||
case ParameterMappingType.WidgetLevel:
|
||||
@@ -73,7 +73,7 @@ export function editableMappingsToParameterMappings(mappings) {
|
||||
break;
|
||||
case MappingType.StaticValue:
|
||||
result.type = ParameterMappingType.StaticValue;
|
||||
result.param = mapping.param.clone();
|
||||
result.param = cloneParameter(mapping.param);
|
||||
result.param.setValue(result.value);
|
||||
result.value = result.param.value;
|
||||
break;
|
||||
@@ -157,7 +157,7 @@ export class ParameterMappingInput extends React.Component {
|
||||
const { onChange, mapping } = this.props;
|
||||
const newMapping = extend({}, mapping, update);
|
||||
if (newMapping.value !== mapping.value) {
|
||||
newMapping.param = newMapping.param.clone();
|
||||
newMapping.param = cloneParameter(newMapping.param);
|
||||
newMapping.param.setValue(newMapping.value);
|
||||
}
|
||||
if (has(update, "type")) {
|
||||
@@ -527,7 +527,7 @@ export class ParameterMappingListInput extends React.Component {
|
||||
|
||||
// static type is different since it's fed param.normalizedValue
|
||||
} else if (type === MappingType.StaticValue) {
|
||||
param = param.clone().setValue(mapping.value);
|
||||
param = cloneParameter(param).setValue(mapping.value);
|
||||
}
|
||||
|
||||
let value = Parameter.getExecutionValue(param);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { size, filter, forEach, extend } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { SortableContainer, SortableElement, DragHandle } from "@/components/sortable";
|
||||
import { $location, $rootScope } from "@/services/ng";
|
||||
import { Parameter } from "@/services/parameters";
|
||||
import location from "@/services/location";
|
||||
import { Parameter, createParameter } from "@/services/parameters";
|
||||
import ParameterApplyButton from "@/components/ParameterApplyButton";
|
||||
import ParameterValueInput from "@/components/ParameterValueInput";
|
||||
import EditParameterSettingsDialog from "./EditParameterSettingsDialog";
|
||||
@@ -12,13 +12,11 @@ import { toHuman } from "@/lib/utils";
|
||||
import "./Parameters.less";
|
||||
|
||||
function updateUrl(parameters) {
|
||||
const params = extend({}, $location.search());
|
||||
const params = extend({}, location.search);
|
||||
parameters.forEach(param => {
|
||||
extend(params, param.toUrlParams());
|
||||
});
|
||||
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
|
||||
$location.search(params);
|
||||
$rootScope.$applyAsync(); // needed for the url to update
|
||||
location.setSearch(params, true);
|
||||
}
|
||||
|
||||
export default class Parameters extends React.Component {
|
||||
@@ -108,14 +106,16 @@ export default class Parameters extends React.Component {
|
||||
|
||||
showParameterSettings = (parameter, index) => {
|
||||
const { onParametersEdit } = this.props;
|
||||
EditParameterSettingsDialog.showModal({ parameter }).result.then(updated => {
|
||||
this.setState(({ parameters }) => {
|
||||
const updatedParameter = extend(parameter, updated);
|
||||
parameters[index] = Parameter.create(updatedParameter, updatedParameter.parentQueryId);
|
||||
onParametersEdit();
|
||||
return { parameters };
|
||||
});
|
||||
});
|
||||
EditParameterSettingsDialog.showModal({ parameter })
|
||||
.result.then(updated => {
|
||||
this.setState(({ parameters }) => {
|
||||
const updatedParameter = extend(parameter, updated);
|
||||
parameters[index] = createParameter(updatedParameter, updatedParameter.parentQueryId);
|
||||
onParametersEdit();
|
||||
return { parameters };
|
||||
});
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
renderParameter(param, index) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import Menu from "antd/lib/menu";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import { $location } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
import settingsMenu from "@/services/settingsMenu";
|
||||
|
||||
function wrapSettingsTab(options, WrappedComponent) {
|
||||
@@ -10,7 +10,7 @@ function wrapSettingsTab(options, WrappedComponent) {
|
||||
}
|
||||
|
||||
return function SettingsTab(props) {
|
||||
const activeItem = settingsMenu.getActiveItem($location.path());
|
||||
const activeItem = settingsMenu.getActiveItem(location.path);
|
||||
return (
|
||||
<div className="settings-screen">
|
||||
<div className="container">
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Layout({ activeTab, children }) {
|
||||
<PageHeader title="Admin" />
|
||||
|
||||
<div className="bg-white tiled">
|
||||
<Tabs className="admin-page-layout-tabs" defaultActiveKey={activeTab} animated={false}>
|
||||
<Tabs className="admin-page-layout-tabs" defaultActiveKey={activeTab} animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="system_status" tab={<a href="admin/status">System Status</a>}>
|
||||
{activeTab === "system_status" ? children : null}
|
||||
</Tabs.TabPane>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export class ErrorHandler {
|
||||
constructor() {
|
||||
this.logToConsole = true;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
process(error) {
|
||||
this.reset();
|
||||
if (this.logToConsole) {
|
||||
// Log raw error object
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
if (error === null || error instanceof PromiseRejectionError) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import debug from "debug";
|
||||
import { react2angular } from "react2angular";
|
||||
import AppHeader from "@/components/app-header/AppHeader";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import { ErrorHandler } from "./error-handler";
|
||||
import template from "./template.html";
|
||||
|
||||
const logger = debug("redash:app-view");
|
||||
|
||||
const handler = new ErrorHandler();
|
||||
|
||||
const layouts = {
|
||||
default: {
|
||||
showHeader: true,
|
||||
bodyClass: false,
|
||||
},
|
||||
fixed: {
|
||||
showHeader: true,
|
||||
bodyClass: "fixed-layout",
|
||||
},
|
||||
defaultSignedOut: {
|
||||
showHeader: false,
|
||||
},
|
||||
};
|
||||
|
||||
function selectLayout(route) {
|
||||
let layout = layouts.default;
|
||||
if (route.layout) {
|
||||
layout = layouts[route.layout] || layouts.default;
|
||||
} else if (!route.authenticated) {
|
||||
layout = layouts.defaultSignedOut;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
class AppViewComponent {
|
||||
constructor($rootScope, $route, Auth) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.layout = layouts.defaultSignedOut;
|
||||
this.handler = handler;
|
||||
|
||||
$rootScope.$on("$routeChangeStart", (event, route) => {
|
||||
this.handler.reset();
|
||||
|
||||
// In case we're handling $routeProvider.otherwise call, there will be no
|
||||
// $$route.
|
||||
const $$route = route.$$route || { authenticated: true };
|
||||
|
||||
if ($$route.authenticated) {
|
||||
// For routes that need authentication, check if session is already
|
||||
// loaded, and load it if not.
|
||||
logger("Requested authenticated route: ", route);
|
||||
if (!Auth.isAuthenticated()) {
|
||||
event.preventDefault();
|
||||
// Auth.requireSession resolves only if session loaded
|
||||
Auth.requireSession().then(() => {
|
||||
this.applyLayout($$route);
|
||||
$route.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$on("$routeChangeSuccess", (event, route) => {
|
||||
const $$route = route.$$route || { authenticated: true };
|
||||
this.applyLayout($$route);
|
||||
if (route.title) {
|
||||
document.title = route.title;
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$on("$routeChangeError", (event, current, previous, rejection) => {
|
||||
const $$route = current.$$route || { authenticated: true };
|
||||
this.applyLayout($$route);
|
||||
throw new PromiseRejectionError(rejection);
|
||||
});
|
||||
}
|
||||
|
||||
applyLayout(route) {
|
||||
this.layout = selectLayout(route);
|
||||
this.$rootScope.bodyClass = this.layout.bodyClass;
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.factory(
|
||||
"$exceptionHandler",
|
||||
() =>
|
||||
function exceptionHandler(exception) {
|
||||
handler.process(exception);
|
||||
}
|
||||
);
|
||||
|
||||
ngModule.component("appHeader", react2angular(AppHeader));
|
||||
|
||||
ngModule.component("appView", {
|
||||
template,
|
||||
controller: AppViewComponent,
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<app-header ng-if="$ctrl.layout.showHeader"></app-header>
|
||||
<div ng-if="$ctrl.handler.error" class="fixed-container" data-test="ErrorMessage">
|
||||
<div class="container">
|
||||
<div class="col-md-8 col-md-push-2">
|
||||
<div class="error-state bg-white tiled">
|
||||
<div class="error-state__icon">
|
||||
<i class="zmdi zmdi-alert-circle-o"></i>
|
||||
</div>
|
||||
<div class="error-state__details">
|
||||
<h4>{{ $ctrl.handler.error.message }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app-content" ng-if="!$ctrl.handler.error" ng-view autoscroll></div>
|
||||
@@ -5,7 +5,7 @@ import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { $location } from "@/services/ng";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { policy } from "@/services/policy";
|
||||
|
||||
@@ -39,10 +39,7 @@ function CreateDashboardDialog({ dialog }) {
|
||||
|
||||
axios.post("api/dashboards", { name }).then(data => {
|
||||
dialog.close();
|
||||
$location
|
||||
.path(`/dashboard/${data.slug}`)
|
||||
.search("edit")
|
||||
.replace();
|
||||
navigateTo(`dashboard/${data.slug}?edit`);
|
||||
});
|
||||
recordEvent("create", "dashboard");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function TextboxWidget(props) {
|
||||
setText(newText);
|
||||
return widget.save();
|
||||
},
|
||||
});
|
||||
}).result.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
const TextboxMenuOptions = [
|
||||
|
||||
@@ -215,7 +215,7 @@ class VisualizationWidget extends React.Component {
|
||||
}
|
||||
|
||||
expandWidget = () => {
|
||||
ExpandedWidgetDialog.showModal({ widget: this.props.widget });
|
||||
ExpandedWidgetDialog.showModal({ widget: this.props.widget }).result.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
editParameterMappings = () => {
|
||||
@@ -223,14 +223,16 @@ class VisualizationWidget extends React.Component {
|
||||
EditParameterMappingsDialog.showModal({
|
||||
dashboard,
|
||||
widget,
|
||||
}).result.then(valuesChanged => {
|
||||
// refresh widget if any parameter value has been updated
|
||||
if (valuesChanged) {
|
||||
onRefresh();
|
||||
}
|
||||
onParameterMappingsChange();
|
||||
this.setState({ localParameters: widget.getLocalParameters() });
|
||||
});
|
||||
})
|
||||
.result.then(valuesChanged => {
|
||||
// refresh widget if any parameter value has been updated
|
||||
if (valuesChanged) {
|
||||
onRefresh();
|
||||
}
|
||||
onParameterMappingsChange();
|
||||
this.setState({ localParameters: widget.getLocalParameters() });
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
renderVisualization() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { keys, some } from "lodash";
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
|
||||
@@ -64,6 +64,10 @@ function EmptyState({
|
||||
inviteUsers: organizationStatus.objectCounters.users > 1,
|
||||
};
|
||||
|
||||
const showCreateDashboardDialog = useCallback(() => {
|
||||
CreateDashboardDialog.showModal().result.catch(() => {}); // ignore dismiss
|
||||
}, []);
|
||||
|
||||
// Show if `onboardingMode=false` or any requested step not completed
|
||||
const shouldShow = !onboardingMode || some(keys(isAvailable), step => isAvailable[step] && !isCompleted[step]);
|
||||
|
||||
@@ -121,7 +125,7 @@ function EmptyState({
|
||||
<Step
|
||||
show={isAvailable.dashboard}
|
||||
completed={isCompleted.dashboard}
|
||||
onClick={() => CreateDashboardDialog.showModal()}
|
||||
onClick={showCreateDashboardDialog}
|
||||
urlText="Create"
|
||||
text="your first Dashboard"
|
||||
/>
|
||||
|
||||
@@ -2,9 +2,7 @@ import { omit, debounce } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import hoistNonReactStatics from "hoist-non-react-statics";
|
||||
import { $route, $routeParams } from "@/services/ng";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { StateStorage } from "./classes/StateStorage";
|
||||
|
||||
export const ControllerType = PropTypes.shape({
|
||||
// values of props declared by wrapped component, current route's locals (`resolve: { ... }`) and title
|
||||
@@ -37,7 +35,7 @@ export const ControllerType = PropTypes.shape({
|
||||
handleError: PropTypes.func.isRequired, // (error) => void
|
||||
});
|
||||
|
||||
export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
|
||||
class ItemsListWrapper extends React.Component {
|
||||
static propTypes = {
|
||||
...omit(WrappedComponent.propTypes, ["controller"]),
|
||||
@@ -59,7 +57,10 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
stateStorage = stateStorage || new StateStorage();
|
||||
const stateStorage = createStateStorage();
|
||||
const itemsSource = createItemsSource();
|
||||
this._itemsSource = itemsSource;
|
||||
|
||||
itemsSource.setState({ ...stateStorage.getState(), validate: false });
|
||||
itemsSource.getCallbackContext = () => this.state;
|
||||
|
||||
@@ -94,9 +95,9 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
itemsSource.onBeforeUpdate = () => {};
|
||||
itemsSource.onAfterUpdate = () => {};
|
||||
itemsSource.onError = () => {};
|
||||
this._itemsSource.onBeforeUpdate = () => {};
|
||||
this._itemsSource.onAfterUpdate = () => {};
|
||||
this._itemsSource.onError = () => {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
@@ -105,14 +106,11 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
// Custom params from items source
|
||||
...params,
|
||||
|
||||
// Add some properties of current route (`$resolve`, title, route params)
|
||||
// ANGULAR_REMOVE_ME Revisit when some React router will be used
|
||||
title: $route.current.title,
|
||||
...$routeParams,
|
||||
...omit($route.current.locals, ["$scope", "$template"]),
|
||||
title: this.props.currentRoute.title,
|
||||
...this.props.routeParams,
|
||||
|
||||
// Add to params all props except of own ones
|
||||
...omit(this.props, ["onError", "children"]),
|
||||
...omit(this.props, ["onError", "children", "currentRoute", "routeParams"]),
|
||||
};
|
||||
return {
|
||||
...rest,
|
||||
|
||||
@@ -141,11 +141,7 @@ export class ItemsSource {
|
||||
|
||||
handleError = error => {
|
||||
if (isFunction(this.onError)) {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
error = new PromiseRejectionError(error);
|
||||
}
|
||||
this.onError(error);
|
||||
this.onError(new PromiseRejectionError(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaults } from "lodash";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { $location } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
import { parse as parseOrderBy, compile as compileOrderBy } from "./Sorter";
|
||||
|
||||
export class StateStorage {
|
||||
@@ -26,7 +26,7 @@ export class StateStorage {
|
||||
export class UrlStateStorage extends StateStorage {
|
||||
getState() {
|
||||
const defaultState = super.getState();
|
||||
const params = $location.search();
|
||||
const params = location.search;
|
||||
|
||||
const searchTerm = params.q || "";
|
||||
|
||||
@@ -47,11 +47,14 @@ export class UrlStateStorage extends StateStorage {
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setState({ page, itemsPerPage, orderByField, orderByReverse, searchTerm }) {
|
||||
$location.search({
|
||||
page,
|
||||
page_size: itemsPerPage,
|
||||
order: compileOrderBy(orderByField, orderByReverse),
|
||||
q: searchTerm !== "" ? searchTerm : null,
|
||||
});
|
||||
location.setSearch(
|
||||
{
|
||||
page,
|
||||
page_size: itemsPerPage,
|
||||
order: compileOrderBy(orderByField, orderByReverse),
|
||||
q: searchTerm !== "" ? searchTerm : null,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Input from "antd/lib/input";
|
||||
import Button from "antd/lib/button";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import CodeBlock from "@/components/CodeBlock";
|
||||
import { $http } from "@/services/ng";
|
||||
import { axios } from "@/services/axios";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import notification from "@/services/notification";
|
||||
|
||||
@@ -18,13 +18,13 @@ function ApiKeyDialog({ dialog, ...props }) {
|
||||
|
||||
const regenerateQueryApiKey = useCallback(() => {
|
||||
setUpdatingApiKey(true);
|
||||
$http
|
||||
axios
|
||||
.post(`api/queries/${query.id}/regenerate_api_key`)
|
||||
.success(data => {
|
||||
.then(data => {
|
||||
setUpdatingApiKey(false);
|
||||
setQuery(extend(query.clone(), { api_key: data.api_key }));
|
||||
})
|
||||
.error(() => {
|
||||
.catch(() => {
|
||||
setUpdatingApiKey(false);
|
||||
notification.error("Failed to update API key");
|
||||
});
|
||||
|
||||
@@ -24,7 +24,9 @@ export class TagsControl extends React.Component {
|
||||
};
|
||||
|
||||
editTags = (tags, getAvailableTags) => {
|
||||
EditTagsDialog.showModal({ tags, getAvailableTags }).result.then(this.props.onEdit);
|
||||
EditTagsDialog.showModal({ tags, getAvailableTags })
|
||||
.result.then(this.props.onEdit)
|
||||
.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
renderEditButton() {
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class UserEdit extends React.Component {
|
||||
}
|
||||
|
||||
changePassword = () => {
|
||||
ChangePasswordDialog.showModal({ user: this.props.user });
|
||||
ChangePasswordDialog.showModal({ user: this.props.user }).result.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
sendPasswordReset = () => {
|
||||
|
||||
@@ -5,26 +5,18 @@ import "core-js/fn/typed/array-buffer";
|
||||
import "@/assets/images/avatar.svg";
|
||||
|
||||
import * as Pace from "pace-progress";
|
||||
import debug from "debug";
|
||||
import angular from "angular";
|
||||
import ngRoute from "angular-route";
|
||||
import { each, isFunction, extend } from "lodash";
|
||||
|
||||
import initAppView from "@/components/app-view";
|
||||
import DialogWrapper from "@/components/DialogWrapper";
|
||||
import organizationStatus from "@/services/organizationStatus";
|
||||
import { isFunction } from "lodash";
|
||||
import url from "@/services/url";
|
||||
|
||||
import "./antd-spinner";
|
||||
import moment from "moment";
|
||||
|
||||
const logger = debug("redash:config");
|
||||
|
||||
Pace.options.shouldHandlePushState = (prevUrl, newUrl) => {
|
||||
// Show pace progress bar only if URL path changed; when query params
|
||||
// or hash changed - ignore that history event
|
||||
const [prevPrefix] = prevUrl.split("?");
|
||||
const [newPrefix] = newUrl.split("?");
|
||||
return prevPrefix !== newPrefix;
|
||||
prevUrl = url.parse(prevUrl);
|
||||
newUrl = url.parse(newUrl);
|
||||
return prevUrl.pathname !== newUrl.pathname;
|
||||
};
|
||||
|
||||
moment.updateLocale("en", {
|
||||
@@ -45,10 +37,6 @@ moment.updateLocale("en", {
|
||||
},
|
||||
});
|
||||
|
||||
const requirements = [ngRoute];
|
||||
|
||||
const ngModule = angular.module("app", requirements);
|
||||
|
||||
function registerAll(context) {
|
||||
const modules = context
|
||||
.keys()
|
||||
@@ -58,7 +46,7 @@ function registerAll(context) {
|
||||
return modules
|
||||
.filter(isFunction)
|
||||
.filter(f => f.init)
|
||||
.map(f => f(ngModule));
|
||||
.map(f => f());
|
||||
}
|
||||
|
||||
function requireImages() {
|
||||
@@ -72,59 +60,11 @@ function registerExtensions() {
|
||||
registerAll(context);
|
||||
}
|
||||
|
||||
function registerServices() {
|
||||
const context = require.context("@/services", true, /^((?![\\/.]test[\\./]).)*\.js$/);
|
||||
registerAll(context);
|
||||
}
|
||||
|
||||
function registerVisualizations() {
|
||||
const context = require.context("@/visualizations", true, /^((?![\\/.]test[\\./]).)*\.jsx?$/);
|
||||
registerAll(context);
|
||||
}
|
||||
|
||||
function registerPages() {
|
||||
const context = require.context("@/pages", true, /^((?![\\/.]test[\\./]).)*\.jsx?$/);
|
||||
const routesCollection = registerAll(context);
|
||||
routesCollection.forEach(routes => {
|
||||
ngModule.config($routeProvider => {
|
||||
each(routes, (route, path) => {
|
||||
logger("Registering route: %s", path);
|
||||
route.authenticated = route.authenticated !== false; // could be set to `false` do disable auth
|
||||
if (route.authenticated) {
|
||||
route.resolve = extend(
|
||||
{
|
||||
__organizationStatus: () => organizationStatus.refresh(),
|
||||
},
|
||||
route.resolve
|
||||
);
|
||||
}
|
||||
$routeProvider.when(path, route);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ngModule.config($routeProvider => {
|
||||
$routeProvider.otherwise({
|
||||
resolve: {
|
||||
// Ugly hack to show 404 when hitting an unknown route.
|
||||
error: () => {
|
||||
const error = { status: 404 };
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
requireImages();
|
||||
registerServices();
|
||||
initAppView(ngModule);
|
||||
registerPages();
|
||||
registerExtensions();
|
||||
registerVisualizations();
|
||||
|
||||
ngModule.run($q => {
|
||||
DialogWrapper.Promise = $q;
|
||||
});
|
||||
|
||||
export default ngModule;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="app" ng-strict-di>
|
||||
<head lang="en">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8">
|
||||
<base href="/">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta charset="UTF-8" />
|
||||
<base href="/" />
|
||||
<title>Redash</title>
|
||||
<script src="/static/unsupportedRedirect.js" async></script>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png" />
|
||||
</head>
|
||||
|
||||
<body ng-class="bodyClass">
|
||||
<body>
|
||||
<section>
|
||||
<app-view></app-view>
|
||||
<div id="application-root"></div>
|
||||
<div class="loading-indicator">
|
||||
<div id="css-logo">
|
||||
<div id="circle">
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import ngModule from "@/config";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import ApplicationArea from "@/components/ApplicationArea";
|
||||
|
||||
ngModule.config(($locationProvider, $compileProvider) => {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|data|tel|sms|mailto):/);
|
||||
$locationProvider.html5Mode(true);
|
||||
import "@/config";
|
||||
|
||||
import offlineListener from "@/services/offline-listener";
|
||||
|
||||
ReactDOM.render(<ApplicationArea />, document.getElementById("application-root"), () => {
|
||||
offlineListener.init();
|
||||
});
|
||||
|
||||
export default ngModule;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { isObject } from "lodash";
|
||||
|
||||
export default class PromiseRejectionError extends Error {
|
||||
constructor(rejection) {
|
||||
let message;
|
||||
if (rejection.status !== undefined) {
|
||||
if (isObject(rejection) && rejection.status !== undefined) {
|
||||
if (rejection.status === 404) {
|
||||
message = "It seems like the page you're looking for cannot be found.";
|
||||
} else if (rejection.status === 403 || rejection.status === 401) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isObject, cloneDeep, each, extend } from "lodash";
|
||||
import moment from "moment";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
|
||||
@@ -158,44 +157,3 @@ export function formatColumnValue(value, columnType = null) {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function routesToAngularRoutes(routes, template) {
|
||||
const result = {};
|
||||
template = extend({}, template); // convert to object
|
||||
each(routes, ({ path, title, key, ...resolve }) => {
|
||||
// Convert to functions
|
||||
each(resolve, (value, prop) => {
|
||||
resolve[prop] = () => value;
|
||||
});
|
||||
|
||||
result[path] = {
|
||||
...template,
|
||||
title,
|
||||
// keep `resolve` from `template` (if exists)
|
||||
resolve: {
|
||||
...template.resolve,
|
||||
...resolve,
|
||||
currentPage: () => key,
|
||||
},
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// ANGULAR_REMOVE_ME
|
||||
export function cleanAngularProps(value) {
|
||||
// remove all props that start with '$$' - that's what `angular.toJson` does
|
||||
const omitAngularProps = obj => {
|
||||
each(obj, (v, k) => {
|
||||
if (("" + k).startsWith("$$")) {
|
||||
delete obj[k];
|
||||
} else {
|
||||
obj[k] = isObject(v) ? omitAngularProps(v) : v;
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
const result = cloneDeep(value);
|
||||
return isObject(result) ? omitAngularProps(result) : result;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="app" ng-strict-di>
|
||||
<head lang="en">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8">
|
||||
<base href="{{base_href}}">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta charset="UTF-8" />
|
||||
<base href="{{base_href}}" />
|
||||
<title>Redash</title>
|
||||
<script src="/static/unsupportedRedirect.js" async></script>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png" />
|
||||
</head>
|
||||
|
||||
<body ng-class="bodyClass">
|
||||
<body>
|
||||
<section>
|
||||
<app-view></app-view>
|
||||
<div id="application-root"></div>
|
||||
<div class="loading-indicator">
|
||||
<div id="css-logo">
|
||||
<div id="circle">
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { flatMap, values } from "lodash";
|
||||
import React from "react";
|
||||
import { axios } from "@/services/axios";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import Alert from "antd/lib/alert";
|
||||
import Tabs from "antd/lib/tabs";
|
||||
import * as Grid from "antd/lib/grid";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import Layout from "@/components/admin/Layout";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { CounterCard, WorkersTable, QueuesTable, OtherJobsTable } from "@/components/admin/RQStatus";
|
||||
|
||||
import { $location, $rootScope } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import moment from "moment";
|
||||
|
||||
class Jobs extends React.Component {
|
||||
state = {
|
||||
activeTab: $location.hash(),
|
||||
activeTab: location.hash,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
|
||||
@@ -82,8 +82,7 @@ class Jobs extends React.Component {
|
||||
const { isLoading, error, queueCounters, startedJobs, overallCounters, workers, activeTab } = this.state;
|
||||
|
||||
const changeTab = newTab => {
|
||||
$location.hash(newTab);
|
||||
$rootScope.$applyAsync();
|
||||
location.setHash(newTab);
|
||||
this.setState({ activeTab: newTab });
|
||||
};
|
||||
|
||||
@@ -122,21 +121,14 @@ class Jobs extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("pageJobs", react2angular(Jobs));
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/admin/queries/jobs",
|
||||
title: "RQ Status",
|
||||
key: "jobs",
|
||||
},
|
||||
],
|
||||
{
|
||||
template: "<page-jobs></page-jobs>",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/admin/queries/jobs",
|
||||
title: "RQ Status",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <Jobs {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { map } from "lodash";
|
||||
import React from "react";
|
||||
import { axios } from "@/services/axios";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import Switch from "antd/lib/switch";
|
||||
import * as Grid from "antd/lib/grid";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import Paginator from "@/components/Paginator";
|
||||
import { QueryTagsControl } from "@/components/tags-control/TagsControl";
|
||||
import SchedulePhrase from "@/components/queries/SchedulePhrase";
|
||||
import TimeAgo from "@/components/TimeAgo";
|
||||
import Layout from "@/components/admin/Layout";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import { wrap as itemsList, ControllerType } from "@/components/items-list/ItemsList";
|
||||
import { ItemsSource } from "@/components/items-list/classes/ItemsSource";
|
||||
@@ -21,7 +22,6 @@ import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTab
|
||||
|
||||
import { Query } from "@/services/query";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
class OutdatedQueries extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -147,51 +147,43 @@ class OutdatedQueries extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageOutdatedQueries",
|
||||
react2angular(
|
||||
itemsList(
|
||||
OutdatedQueries,
|
||||
new ItemsSource({
|
||||
doRequest(request, context) {
|
||||
return (
|
||||
axios
|
||||
.get("/api/admin/queries/outdated")
|
||||
// eslint-disable-next-line camelcase
|
||||
.then(({ queries, updated_at }) => {
|
||||
context.setCustomParams({ lastUpdatedAt: parseFloat(updated_at) });
|
||||
return queries;
|
||||
})
|
||||
);
|
||||
},
|
||||
processResults(items) {
|
||||
return map(items, item => new Query(item));
|
||||
},
|
||||
isPlainList: true,
|
||||
}),
|
||||
new StateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/admin/queries/outdated",
|
||||
title: "Outdated Queries",
|
||||
key: "outdated_queries",
|
||||
const OutdatedQueriesPage = itemsList(
|
||||
OutdatedQueries,
|
||||
() =>
|
||||
new ItemsSource({
|
||||
doRequest(request, context) {
|
||||
return (
|
||||
axios
|
||||
.get("/api/admin/queries/outdated")
|
||||
// eslint-disable-next-line camelcase
|
||||
.then(({ queries, updated_at }) => {
|
||||
context.setCustomParams({ lastUpdatedAt: parseFloat(updated_at) });
|
||||
return queries;
|
||||
})
|
||||
);
|
||||
},
|
||||
],
|
||||
{
|
||||
template: '<page-outdated-queries on-error="handleError"></page-outdated-queries>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
processResults(items) {
|
||||
return map(items, item => new Query(item));
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
isPlainList: true,
|
||||
}),
|
||||
() => new StateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
);
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/admin/queries/outdated",
|
||||
title: "Outdated Queries",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<OutdatedQueriesPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "outdated_queries" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@ import { omit } from "lodash";
|
||||
import React from "react";
|
||||
import { axios } from "@/services/axios";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import Layout from "@/components/admin/Layout";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import * as StatusBlock from "@/components/admin/StatusBlock";
|
||||
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
import "./system-status.less";
|
||||
|
||||
@@ -56,11 +56,7 @@ class SystemStatus extends React.Component {
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
error = new PromiseRejectionError(error);
|
||||
}
|
||||
this.props.onError(error);
|
||||
this.props.onError(new PromiseRejectionError(error));
|
||||
});
|
||||
this._refreshTimer = setTimeout(this.refresh, 60 * 1000);
|
||||
};
|
||||
@@ -89,26 +85,14 @@ class SystemStatus extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("pageSystemStatus", react2angular(SystemStatus));
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/admin/status",
|
||||
title: "System Status",
|
||||
key: "system_status",
|
||||
},
|
||||
],
|
||||
{
|
||||
template: '<page-system-status on-error="handleError"></page-system-status>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/admin/status",
|
||||
title: "System Status",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <SystemStatus {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { head, includes, trim, template, values } from "lodash";
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
import { head, includes, trim, template } from "lodash";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { $route } from "@/services/ng";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import notification from "@/services/notification";
|
||||
import AlertService from "@/services/alert";
|
||||
import { Query as QueryService } from "@/services/query";
|
||||
@@ -15,7 +16,6 @@ import AlertView from "./AlertView";
|
||||
import AlertEdit from "./AlertEdit";
|
||||
import AlertNew from "./AlertNew";
|
||||
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
const MODES = {
|
||||
@@ -34,6 +34,18 @@ export function getDefaultName(alert) {
|
||||
}
|
||||
|
||||
class AlertPage extends React.Component {
|
||||
static propTypes = {
|
||||
mode: PropTypes.oneOf(values(MODES)),
|
||||
alertId: PropTypes.string,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
mode: null,
|
||||
alertId: null,
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
_isMounted = false;
|
||||
|
||||
state = {
|
||||
@@ -46,7 +58,7 @@ class AlertPage extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
const { mode } = $route.current.locals;
|
||||
const { mode } = this.props;
|
||||
this.setState({ mode });
|
||||
|
||||
if (mode === MODES.NEW) {
|
||||
@@ -62,7 +74,7 @@ class AlertPage extends React.Component {
|
||||
canEdit: true,
|
||||
});
|
||||
} else {
|
||||
const { alertId } = $route.current.params;
|
||||
const { alertId } = this.props;
|
||||
AlertService.get({ id: alertId })
|
||||
.then(alert => {
|
||||
if (this._isMounted) {
|
||||
@@ -84,7 +96,7 @@ class AlertPage extends React.Component {
|
||||
})
|
||||
.catch(err => {
|
||||
if (this._isMounted) {
|
||||
throw new PromiseRejectionError(err);
|
||||
this.props.onError(new PromiseRejectionError(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -103,7 +115,7 @@ class AlertPage extends React.Component {
|
||||
return AlertService.save(alert)
|
||||
.then(alert => {
|
||||
notification.success("Saved.");
|
||||
navigateTo(`/alerts/${alert.id}`, true, false);
|
||||
navigateTo(`alerts/${alert.id}`, true);
|
||||
this.setState({ alert, mode: MODES.VIEW });
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -159,7 +171,7 @@ class AlertPage extends React.Component {
|
||||
return AlertService.delete(alert)
|
||||
.then(() => {
|
||||
notification.success("Alert deleted successfully.");
|
||||
navigateTo("/alerts");
|
||||
navigateTo("alerts");
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error("Failed deleting alert.");
|
||||
@@ -192,13 +204,13 @@ class AlertPage extends React.Component {
|
||||
|
||||
edit = () => {
|
||||
const { id } = this.state.alert;
|
||||
navigateTo(`/alerts/${id}/edit`, true, false);
|
||||
navigateTo(`alerts/${id}/edit`, true);
|
||||
this.setState({ mode: MODES.EDIT });
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
const { id } = this.state.alert;
|
||||
navigateTo(`/alerts/${id}`, true, false);
|
||||
navigateTo(`alerts/${id}`, true);
|
||||
this.setState({ mode: MODES.VIEW });
|
||||
};
|
||||
|
||||
@@ -229,47 +241,51 @@ class AlertPage extends React.Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container alert-page">
|
||||
{mode === MODES.NEW && <AlertNew {...commonProps} />}
|
||||
{mode === MODES.VIEW && (
|
||||
<AlertView canEdit={canEdit} onEdit={this.edit} muted={muted} unmute={this.unmute} {...commonProps} />
|
||||
)}
|
||||
{mode === MODES.EDIT && <AlertEdit cancel={this.cancel} {...commonProps} />}
|
||||
<div className="alert-page">
|
||||
<div className="container">
|
||||
{mode === MODES.NEW && <AlertNew {...commonProps} />}
|
||||
{mode === MODES.VIEW && (
|
||||
<AlertView canEdit={canEdit} onEdit={this.edit} muted={muted} unmute={this.unmute} {...commonProps} />
|
||||
)}
|
||||
{mode === MODES.EDIT && <AlertEdit cancel={this.cancel} {...commonProps} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("alertPage", react2angular(AlertPage));
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/alerts/new",
|
||||
title: "New Alert",
|
||||
mode: MODES.NEW,
|
||||
},
|
||||
{
|
||||
path: "/alerts/:alertId",
|
||||
title: "Alert",
|
||||
mode: MODES.VIEW,
|
||||
},
|
||||
{
|
||||
path: "/alerts/:alertId/edit",
|
||||
title: "Alert",
|
||||
mode: MODES.EDIT,
|
||||
},
|
||||
],
|
||||
{
|
||||
template: "<alert-page></alert-page>",
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/alerts/new",
|
||||
title: "New Alert",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <AlertPage {...currentRoute.routeParams} mode={MODES.NEW} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/alerts/:alertId([0-9]+)",
|
||||
title: "Alert",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <AlertPage {...currentRoute.routeParams} mode={MODES.VIEW} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/alerts/:alertId([0-9]+)/edit",
|
||||
title: "Alert",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <AlertPage {...currentRoute.routeParams} mode={MODES.EDIT} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -127,7 +127,7 @@ export default class AlertDestinations extends React.Component {
|
||||
notification.error("Failed saving subscription.");
|
||||
});
|
||||
},
|
||||
});
|
||||
}).result.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
onUserEmailToggle = sub => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import { toUpper } from "lodash";
|
||||
import React from "react";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import Paginator from "@/components/Paginator";
|
||||
import EmptyState from "@/components/empty-state/EmptyState";
|
||||
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { wrap as liveItemsList, ControllerType } from "@/components/items-list/ItemsList";
|
||||
import { ResourceItemsSource } from "@/components/items-list/classes/ItemsSource";
|
||||
import { StateStorage } from "@/components/items-list/classes/StateStorage";
|
||||
@@ -14,7 +13,6 @@ import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
|
||||
|
||||
import Alert from "@/services/alert";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
export const STATE_CLASS = {
|
||||
unknown: "label-warning",
|
||||
@@ -70,79 +68,73 @@ class AlertsList extends React.Component {
|
||||
const { controller } = this.props;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<PageHeader title={controller.params.title} />
|
||||
<div className="m-l-15 m-r-15">
|
||||
{!controller.isLoaded && <LoadingState className="" />}
|
||||
{controller.isLoaded && controller.isEmpty && (
|
||||
<EmptyState
|
||||
icon="fa fa-bell-o"
|
||||
illustration="alert"
|
||||
description="Get notified on certain events"
|
||||
helpLink="https://redash.io/help/user-guide/alerts/"
|
||||
showAlertStep
|
||||
/>
|
||||
)}
|
||||
{controller.isLoaded && !controller.isEmpty && (
|
||||
<div className="table-responsive bg-white tiled">
|
||||
<ItemsTable
|
||||
items={controller.pageItems}
|
||||
columns={this.listColumns}
|
||||
orderByField={controller.orderByField}
|
||||
orderByReverse={controller.orderByReverse}
|
||||
toggleSorting={controller.toggleSorting}
|
||||
<div className="page-alerts-list">
|
||||
<div className="container">
|
||||
<PageHeader title={controller.params.title} />
|
||||
<div className="m-l-15 m-r-15">
|
||||
{!controller.isLoaded && <LoadingState className="" />}
|
||||
{controller.isLoaded && controller.isEmpty && (
|
||||
<EmptyState
|
||||
icon="fa fa-bell-o"
|
||||
illustration="alert"
|
||||
description="Get notified on certain events"
|
||||
helpLink="https://redash.io/help/user-guide/alerts/"
|
||||
showAlertStep
|
||||
/>
|
||||
<Paginator
|
||||
totalCount={controller.totalItemsCount}
|
||||
itemsPerPage={controller.itemsPerPage}
|
||||
page={controller.page}
|
||||
onChange={page => controller.updatePagination({ page })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{controller.isLoaded && !controller.isEmpty && (
|
||||
<div className="table-responsive bg-white tiled">
|
||||
<ItemsTable
|
||||
items={controller.pageItems}
|
||||
columns={this.listColumns}
|
||||
orderByField={controller.orderByField}
|
||||
orderByReverse={controller.orderByReverse}
|
||||
toggleSorting={controller.toggleSorting}
|
||||
/>
|
||||
<Paginator
|
||||
totalCount={controller.totalItemsCount}
|
||||
itemsPerPage={controller.itemsPerPage}
|
||||
page={controller.page}
|
||||
onChange={page => controller.updatePagination({ page })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageAlertsList",
|
||||
react2angular(
|
||||
liveItemsList(
|
||||
AlertsList,
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest() {
|
||||
return {};
|
||||
},
|
||||
getResource() {
|
||||
return Alert.query.bind(Alert);
|
||||
},
|
||||
}),
|
||||
new StateStorage({ orderByField: "created_at", orderByReverse: true, itemsPerPage: 20 })
|
||||
)
|
||||
)
|
||||
);
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/alerts",
|
||||
title: "Alerts",
|
||||
key: "alerts",
|
||||
const AlertsListPage = liveItemsList(
|
||||
AlertsList,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest() {
|
||||
return {};
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-alerts-list on-error="handleError"></page-alerts-list>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
getResource() {
|
||||
return Alert.query.bind(Alert);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}),
|
||||
() => new StateStorage({ orderByField: "created_at", orderByReverse: true, itemsPerPage: 20 })
|
||||
);
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/alerts",
|
||||
title: "Alerts",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<AlertsListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "alerts" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import Paginator from "@/components/Paginator";
|
||||
import { DashboardTagsControl } from "@/components/tags-control/TagsControl";
|
||||
@@ -8,7 +8,7 @@ import { DashboardTagsControl } from "@/components/tags-control/TagsControl";
|
||||
import { wrap as itemsList, ControllerType } from "@/components/items-list/ItemsList";
|
||||
import { ResourceItemsSource } from "@/components/items-list/classes/ItemsSource";
|
||||
import { UrlStateStorage } from "@/components/items-list/classes/StateStorage";
|
||||
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import * as Sidebar from "@/components/items-list/components/Sidebar";
|
||||
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
|
||||
@@ -16,7 +16,6 @@ import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTab
|
||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||
|
||||
import { Dashboard } from "@/services/dashboard";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
import DashboardListEmptyState from "./DashboardListEmptyState";
|
||||
|
||||
@@ -75,106 +74,113 @@ class DashboardList extends React.Component {
|
||||
render() {
|
||||
const { controller } = this.props;
|
||||
return (
|
||||
<div className="container">
|
||||
<PageHeader title={controller.params.title} />
|
||||
<Layout className="m-l-15 m-r-15">
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput
|
||||
placeholder="Search Dashboards..."
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
/>
|
||||
<Sidebar.Menu items={this.sidebarMenu} selected={controller.params.currentPage} />
|
||||
<Sidebar.Tags url="api/dashboards/tags" onChange={controller.updateSelectedTags} />
|
||||
<Sidebar.PageSizeSelect
|
||||
className="m-b-10"
|
||||
options={controller.pageSizeOptions}
|
||||
value={controller.itemsPerPage}
|
||||
onChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
||||
/>
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
{controller.isLoaded ? (
|
||||
<div data-test="DashboardLayoutContent">
|
||||
{controller.isEmpty ? (
|
||||
<DashboardListEmptyState
|
||||
page={controller.params.currentPage}
|
||||
searchTerm={controller.searchTerm}
|
||||
selectedTags={controller.selectedTags}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-white tiled table-responsive">
|
||||
<ItemsTable
|
||||
items={controller.pageItems}
|
||||
columns={this.listColumns}
|
||||
orderByField={controller.orderByField}
|
||||
orderByReverse={controller.orderByReverse}
|
||||
toggleSorting={controller.toggleSorting}
|
||||
<div className="page-dashboard-list">
|
||||
<div className="container">
|
||||
<PageHeader title={controller.params.title} />
|
||||
<Layout className="m-l-15 m-r-15">
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput
|
||||
placeholder="Search Dashboards..."
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
/>
|
||||
<Sidebar.Menu items={this.sidebarMenu} selected={controller.params.currentPage} />
|
||||
<Sidebar.Tags url="api/dashboards/tags" onChange={controller.updateSelectedTags} />
|
||||
<Sidebar.PageSizeSelect
|
||||
className="m-b-10"
|
||||
options={controller.pageSizeOptions}
|
||||
value={controller.itemsPerPage}
|
||||
onChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
||||
/>
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
{controller.isLoaded ? (
|
||||
<div data-test="DashboardLayoutContent">
|
||||
{controller.isEmpty ? (
|
||||
<DashboardListEmptyState
|
||||
page={controller.params.currentPage}
|
||||
searchTerm={controller.searchTerm}
|
||||
selectedTags={controller.selectedTags}
|
||||
/>
|
||||
<Paginator
|
||||
totalCount={controller.totalItemsCount}
|
||||
itemsPerPage={controller.itemsPerPage}
|
||||
page={controller.page}
|
||||
onChange={page => controller.updatePagination({ page })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingState />
|
||||
)}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
) : (
|
||||
<div className="bg-white tiled table-responsive">
|
||||
<ItemsTable
|
||||
items={controller.pageItems}
|
||||
columns={this.listColumns}
|
||||
orderByField={controller.orderByField}
|
||||
orderByReverse={controller.orderByReverse}
|
||||
toggleSorting={controller.toggleSorting}
|
||||
/>
|
||||
<Paginator
|
||||
totalCount={controller.totalItemsCount}
|
||||
itemsPerPage={controller.itemsPerPage}
|
||||
page={controller.page}
|
||||
onChange={page => controller.updatePagination({ page })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingState />
|
||||
)}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageDashboardList",
|
||||
react2angular(
|
||||
itemsList(
|
||||
DashboardList,
|
||||
new ResourceItemsSource({
|
||||
getResource({ params: { currentPage } }) {
|
||||
return {
|
||||
all: Dashboard.query.bind(Dashboard),
|
||||
favorites: Dashboard.favorites.bind(Dashboard),
|
||||
}[currentPage];
|
||||
},
|
||||
getItemProcessor() {
|
||||
return item => new Dashboard(item);
|
||||
},
|
||||
}),
|
||||
new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/dashboards",
|
||||
title: "Dashboards",
|
||||
key: "all",
|
||||
const DashboardListPage = itemsList(
|
||||
DashboardList,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
getResource({ params: { currentPage } }) {
|
||||
return {
|
||||
all: Dashboard.query.bind(Dashboard),
|
||||
favorites: Dashboard.favorites.bind(Dashboard),
|
||||
}[currentPage];
|
||||
},
|
||||
{
|
||||
path: "/dashboards/favorites",
|
||||
title: "Favorite Dashboards",
|
||||
key: "favorites",
|
||||
getItemProcessor() {
|
||||
return item => new Dashboard(item);
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-dashboard-list on-error="handleError"></page-dashboard-list>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
}),
|
||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
);
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/dashboards",
|
||||
title: "Dashboards",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<DashboardListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "all" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboards/favorites",
|
||||
title: "Favorite Dashboards",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<DashboardListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "favorites" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import { map, isEmpty, includes } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import Button from "antd/lib/button";
|
||||
import Checkbox from "antd/lib/checkbox";
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
@@ -10,15 +9,16 @@ import Menu from "antd/lib/menu";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import DashboardGrid from "@/components/dashboards/DashboardGrid";
|
||||
import FavoritesControl from "@/components/FavoritesControl";
|
||||
import EditInPlace from "@/components/EditInPlace";
|
||||
import { DashboardTagsControl } from "@/components/tags-control/TagsControl";
|
||||
import Parameters from "@/components/Parameters";
|
||||
import Filters from "@/components/Filters";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { Dashboard } from "@/services/dashboard";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { $route } from "@/services/ng";
|
||||
import getTags from "@/services/getTags";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { policy } from "@/services/policy";
|
||||
@@ -378,32 +378,45 @@ DashboardComponent.propTypes = {
|
||||
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
function DashboardPage() {
|
||||
function DashboardPage({ dashboardSlug, onError }) {
|
||||
const [dashboard, setDashboard] = useState(null);
|
||||
const onErrorRef = useRef();
|
||||
onErrorRef.current = onError;
|
||||
|
||||
useEffect(() => {
|
||||
Dashboard.get({ slug: $route.current.params.dashboardSlug })
|
||||
Dashboard.get({ slug: dashboardSlug })
|
||||
.then(dashboardData => {
|
||||
recordEvent("view", "dashboard", dashboardData.id);
|
||||
setDashboard(dashboardData);
|
||||
})
|
||||
.catch(error => {
|
||||
throw new PromiseRejectionError(error);
|
||||
onErrorRef.current(new PromiseRejectionError(error));
|
||||
});
|
||||
}, []);
|
||||
}, [dashboardSlug]);
|
||||
|
||||
return <div className="container">{dashboard && <DashboardComponent dashboard={dashboard} />}</div>;
|
||||
return (
|
||||
<div className="dashboard-page">
|
||||
<div className="container">{dashboard && <DashboardComponent dashboard={dashboard} />}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("dashboardPage", react2angular(DashboardPage));
|
||||
DashboardPage.propTypes = {
|
||||
dashboardSlug: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
return {
|
||||
"/dashboard/:dashboardSlug": {
|
||||
template: "<dashboard-page></dashboard-page>",
|
||||
reloadOnSearch: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
DashboardPage.defaultProps = {
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/dashboard/:dashboardSlug",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <DashboardPage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@import '../../assets/less/inc/variables';
|
||||
@import '../../components/app-header/AppHeader.less';
|
||||
@import "~@/assets/less/inc/variables";
|
||||
@import "~@/components/ApplicationArea/ApplicationHeader/index.less";
|
||||
|
||||
/****
|
||||
grid bg - based on 6 cols, 35px rows and 15px spacing
|
||||
****/
|
||||
|
||||
// let the bg go all the way to the bottom
|
||||
dashboard-page, dashboard-page .container {
|
||||
.dashboard-page,
|
||||
.dashboard-page .container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
@@ -22,7 +23,7 @@ dashboard-page, dashboard-page .container {
|
||||
.dashboard-header {
|
||||
padding: 0 15px !important;
|
||||
margin: 0 0 10px !important;
|
||||
position: -webkit-sticky; // required for Safari
|
||||
position: -webkit-sticky; // required for Safari
|
||||
position: sticky;
|
||||
background: #f6f7f9;
|
||||
z-index: 99;
|
||||
@@ -81,26 +82,27 @@ dashboard-page, dashboard-page .container {
|
||||
width: 45px;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
content: "";
|
||||
animation: saving 2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-error] {
|
||||
color: #F44336;
|
||||
color: #f44336;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes saving {
|
||||
0%, 100% {
|
||||
content: '.';
|
||||
0%,
|
||||
100% {
|
||||
content: ".";
|
||||
}
|
||||
33% {
|
||||
content: '..';
|
||||
content: "..";
|
||||
}
|
||||
66% {
|
||||
content: '...';
|
||||
content: "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +113,7 @@ dashboard-page, dashboard-page .container {
|
||||
position: fixed;
|
||||
left: 15px;
|
||||
bottom: 20px;
|
||||
width: calc(~'100% - 30px');
|
||||
width: calc(~"100% - 30px");
|
||||
z-index: 99;
|
||||
box-shadow: fade(@redash-gray, 50%) 0px 7px 29px -3px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
import { isEmpty } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import SignedOutPageWrapper from "@/components/ApplicationArea/SignedOutPageWrapper";
|
||||
import BigMessage from "@/components/BigMessage";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import Parameters from "@/components/Parameters";
|
||||
import DashboardGrid from "@/components/dashboards/DashboardGrid";
|
||||
import Filters from "@/components/Filters";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { Dashboard } from "@/services/dashboard";
|
||||
import { $route as ngRoute } from "@/services/ng";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import logoUrl from "@/assets/images/redash_icon_small.png";
|
||||
import useDashboard from "./useDashboard";
|
||||
@@ -53,16 +53,25 @@ PublicDashboard.propTypes = {
|
||||
};
|
||||
|
||||
class PublicDashboardPage extends React.Component {
|
||||
static propTypes = {
|
||||
token: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
dashboard: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Dashboard.getByToken({ token: ngRoute.current.params.token })
|
||||
Dashboard.getByToken({ token: this.props.token })
|
||||
.then(dashboard => this.setState({ dashboard, loading: false }))
|
||||
.catch(error => {
|
||||
throw new PromiseRejectionError(error);
|
||||
this.props.onError(new PromiseRejectionError(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,24 +99,14 @@ class PublicDashboardPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("publicDashboardPage", react2angular(PublicDashboardPage));
|
||||
|
||||
return {
|
||||
"/public/dashboards/:token": {
|
||||
authenticated: false,
|
||||
template: "<public-dashboard-page></public-dashboard-page>",
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
session: ($route, Auth) => {
|
||||
"ngInject";
|
||||
const token = $route.current.params.token;
|
||||
Auth.setApiKey(token);
|
||||
return Auth.loadConfig();
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/public/dashboards/:token",
|
||||
authenticated: false,
|
||||
render: currentRoute => (
|
||||
<SignedOutPageWrapper key={currentRoute.key} apiKey={currentRoute.routeParams.token}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <PublicDashboardPage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</SignedOutPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
min,
|
||||
} from "lodash";
|
||||
import notification from "@/services/notification";
|
||||
import { $location, $rootScope } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
import { Dashboard, collectDashboardFilters } from "@/services/dashboard";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
@@ -35,11 +35,6 @@ export const DashboardStatusEnum = {
|
||||
SAVING_FAILED: "saving_failed",
|
||||
};
|
||||
|
||||
function updateUrlSearch(...params) {
|
||||
$location.search(...params);
|
||||
$rootScope.$applyAsync();
|
||||
}
|
||||
|
||||
function getAffectedWidgets(widgets, updatedParameters = []) {
|
||||
return !isEmpty(updatedParameters)
|
||||
? widgets.filter(widget =>
|
||||
@@ -69,15 +64,15 @@ function getLimitedRefreshRate(refreshRate) {
|
||||
}
|
||||
|
||||
function getRefreshRateFromUrl() {
|
||||
const refreshRate = parseFloat($location.search().refresh);
|
||||
const refreshRate = parseFloat(location.search.refresh);
|
||||
return isNaN(refreshRate) ? null : getLimitedRefreshRate(refreshRate);
|
||||
}
|
||||
|
||||
function useFullscreenHandler() {
|
||||
const [fullscreen, setFullscreen] = useState(has($location.search(), "fullscreen"));
|
||||
const [fullscreen, setFullscreen] = useState(has(location.search, "fullscreen"));
|
||||
useEffect(() => {
|
||||
document.querySelector("body").classList.toggle("headless", fullscreen);
|
||||
updateUrlSearch("fullscreen", fullscreen ? true : null);
|
||||
document.body.classList.toggle("headless", fullscreen);
|
||||
location.setSearch({ fullscreen: fullscreen ? true : null }, true);
|
||||
}, [fullscreen]);
|
||||
|
||||
const toggleFullscreen = () => setFullscreen(!fullscreen);
|
||||
@@ -88,7 +83,7 @@ function useRefreshRateHandler(refreshDashboard) {
|
||||
const [refreshRate, setRefreshRate] = useState(getRefreshRateFromUrl());
|
||||
|
||||
useEffect(() => {
|
||||
updateUrlSearch("refresh", refreshRate || null);
|
||||
location.setSearch({ refresh: refreshRate || null }, true);
|
||||
if (refreshRate) {
|
||||
const refreshTimer = setInterval(refreshDashboard, refreshRate * 1000);
|
||||
return () => clearInterval(refreshTimer);
|
||||
@@ -99,13 +94,13 @@ function useRefreshRateHandler(refreshDashboard) {
|
||||
}
|
||||
|
||||
function useEditModeHandler(canEditDashboard, widgets) {
|
||||
const [editingLayout, setEditingLayout] = useState(canEditDashboard && has($location.search(), "edit"));
|
||||
const [editingLayout, setEditingLayout] = useState(canEditDashboard && has(location.search, "edit"));
|
||||
const [dashboardStatus, setDashboardStatus] = useState(DashboardStatusEnum.SAVED);
|
||||
const [recentPositions, setRecentPositions] = useState([]);
|
||||
const [doneBtnClickedWhileSaving, setDoneBtnClickedWhileSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateUrlSearch("edit", editingLayout ? true : null);
|
||||
location.setSearch({ edit: editingLayout ? true : null }, true);
|
||||
}, [editingLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -206,7 +201,7 @@ function useDashboard(dashboardData) {
|
||||
aclUrl,
|
||||
context: "dashboard",
|
||||
author: dashboard.user,
|
||||
});
|
||||
}).result.catch(() => {}); // ignore dismiss
|
||||
}, [dashboard]);
|
||||
|
||||
const updateDashboard = useCallback(
|
||||
@@ -266,7 +261,7 @@ function useDashboard(dashboardData) {
|
||||
|
||||
return Promise.all(loadWidgetPromises).then(() => {
|
||||
const queryResults = compact(map(dashboard.widgets, widget => widget.getQueryResult()));
|
||||
const updatedFilters = collectDashboardFilters(dashboard, queryResults, $location.search());
|
||||
const updatedFilters = collectDashboardFilters(dashboard, queryResults, location.search);
|
||||
setFilters(updatedFilters);
|
||||
});
|
||||
},
|
||||
@@ -292,7 +287,9 @@ function useDashboard(dashboardData) {
|
||||
ShareDashboardDialog.showModal({
|
||||
dashboard,
|
||||
hasOnlySafeQueries,
|
||||
}).result.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
|
||||
})
|
||||
.result.catch(() => {}) // ignore dismiss
|
||||
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
|
||||
}, [dashboard, hasOnlySafeQueries]);
|
||||
|
||||
const showAddTextboxDialog = useCallback(() => {
|
||||
@@ -300,7 +297,7 @@ function useDashboard(dashboardData) {
|
||||
dashboard,
|
||||
onConfirm: text =>
|
||||
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard))),
|
||||
});
|
||||
}).result.catch(() => {}); // ignore dismiss
|
||||
}, [dashboard]);
|
||||
|
||||
const showAddWidgetDialog = useCallback(() => {
|
||||
@@ -320,7 +317,7 @@ function useDashboard(dashboardData) {
|
||||
setDashboard(currentDashboard => extend({}, currentDashboard))
|
||||
);
|
||||
}),
|
||||
});
|
||||
}).result.catch(() => {}); // ignore dismiss
|
||||
}, [dashboard]);
|
||||
|
||||
const [refreshRate, setRefreshRate, disableRefreshRate] = useRefreshRateHandler(refreshDashboard);
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import { react2angular } from "react2angular";
|
||||
import { isEmpty, get } from "lodash";
|
||||
import { isEmpty } from "lodash";
|
||||
import DataSource, { IMG_ROOT } from "@/services/data-source";
|
||||
import { policy } from "@/services/policy";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { $route } from "@/services/ng";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import CardsList from "@/components/cards-list/CardsList";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import CreateSourceDialog from "@/components/CreateSourceDialog";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import helper from "@/components/dynamic-form/dynamicFormHelper";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
class DataSourcesList extends React.Component {
|
||||
static propTypes = {
|
||||
isNewDataSourcePage: PropTypes.bool,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isNewDataSourcePage: false,
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
state = {
|
||||
dataSourceTypes: [],
|
||||
dataSources: [],
|
||||
@@ -25,25 +36,27 @@ class DataSourcesList extends React.Component {
|
||||
newDataSourceDialog = null;
|
||||
|
||||
componentDidMount() {
|
||||
Promise.all([DataSource.query(), DataSource.types()]).then(values =>
|
||||
this.setState(
|
||||
{
|
||||
dataSources: values[0],
|
||||
dataSourceTypes: values[1],
|
||||
loading: false,
|
||||
},
|
||||
() => {
|
||||
// all resources are loaded in state
|
||||
if ($route.current.locals.isNewDataSourcePage) {
|
||||
if (policy.canCreateDataSource()) {
|
||||
this.showCreateSourceDialog();
|
||||
} else {
|
||||
navigateTo("/data_sources");
|
||||
Promise.all([DataSource.query(), DataSource.types()])
|
||||
.then(values =>
|
||||
this.setState(
|
||||
{
|
||||
dataSources: values[0],
|
||||
dataSourceTypes: values[1],
|
||||
loading: false,
|
||||
},
|
||||
() => {
|
||||
// all resources are loaded in state
|
||||
if (this.props.isNewDataSourcePage) {
|
||||
if (policy.canCreateDataSource()) {
|
||||
this.showCreateSourceDialog();
|
||||
} else {
|
||||
navigateTo("data_sources", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
.catch(error => this.props.onError(new PromiseRejectionError(error)));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -62,12 +75,7 @@ class DataSourcesList extends React.Component {
|
||||
DataSource.query().then(dataSources => this.setState({ dataSources, loading: false }));
|
||||
return dataSource;
|
||||
})
|
||||
.catch(error => {
|
||||
if (!(error instanceof Error)) {
|
||||
error = new Error(get(error, "data.message", "Failed saving."));
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
.catch(error => Promise.reject(new PromiseRejectionError(error)));
|
||||
};
|
||||
|
||||
showCreateSourceDialog = () => {
|
||||
@@ -88,6 +96,7 @@ class DataSourcesList extends React.Component {
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
navigateTo("data_sources", true);
|
||||
this.newDataSourceDialog = null;
|
||||
});
|
||||
};
|
||||
@@ -139,45 +148,39 @@ class DataSourcesList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageDataSourcesList",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
permission: "admin",
|
||||
title: "Data Sources",
|
||||
path: "data_sources",
|
||||
order: 1,
|
||||
},
|
||||
DataSourcesList
|
||||
)
|
||||
)
|
||||
);
|
||||
const DataSourcesListPage = wrapSettingsTab(
|
||||
{
|
||||
permission: "admin",
|
||||
title: "Data Sources",
|
||||
path: "data_sources",
|
||||
order: 1,
|
||||
},
|
||||
DataSourcesList
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/data_sources",
|
||||
title: "Data Sources",
|
||||
key: "data_sources",
|
||||
},
|
||||
{
|
||||
path: "/data_sources/new",
|
||||
title: "Data Sources",
|
||||
key: "data_sources",
|
||||
isNewDataSourcePage: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
template: "<page-data-sources-list></page-data-sources-list>",
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/data_sources",
|
||||
title: "Data Sources",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <DataSourcesListPage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/data_sources/new",
|
||||
title: "Data Sources",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<DataSourcesListPage {...currentRoute.routeParams} isNewDataSourcePage onError={handleError} />
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { get, find, toUpper } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import Modal from "antd/lib/modal";
|
||||
import DataSource, { IMG_ROOT } from "@/services/data-source";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { $route } from "@/services/ng";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import notification from "@/services/notification";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
@@ -13,9 +12,11 @@ import DynamicForm from "@/components/dynamic-form/DynamicForm";
|
||||
import helper from "@/components/dynamic-form/dynamicFormHelper";
|
||||
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from "@/components/HelpTrigger";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
class EditDataSource extends React.Component {
|
||||
static propTypes = {
|
||||
dataSourceId: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -30,18 +31,14 @@ class EditDataSource extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
DataSource.get({ id: $route.current.params.dataSourceId })
|
||||
DataSource.get({ id: this.props.dataSourceId })
|
||||
.then(dataSource => {
|
||||
const { type } = dataSource;
|
||||
this.setState({ dataSource });
|
||||
DataSource.types().then(types => this.setState({ type: find(types, { type }), loading: false }));
|
||||
})
|
||||
.catch(error => {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
error = new PromiseRejectionError(error);
|
||||
}
|
||||
this.props.onError(error);
|
||||
this.props.onError(new PromiseRejectionError(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +60,7 @@ class EditDataSource extends React.Component {
|
||||
DataSource.delete(dataSource)
|
||||
.then(() => {
|
||||
notification.success("Data source deleted successfully.");
|
||||
navigateTo("/data_sources", true);
|
||||
navigateTo("data_sources");
|
||||
})
|
||||
.catch(() => {
|
||||
callback();
|
||||
@@ -143,20 +140,16 @@ class EditDataSource extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("pageEditDataSource", react2angular(wrapSettingsTab(null, EditDataSource)));
|
||||
const EditDataSourcePage = wrapSettingsTab(null, EditDataSource);
|
||||
|
||||
return {
|
||||
"/data_sources/:dataSourceId": {
|
||||
template: '<page-edit-data-source on-error="handleError"></page-edit-data-source>',
|
||||
title: "Data Sources",
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/data_sources/:dataSourceId([0-9]+)",
|
||||
title: "Data Sources",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <EditDataSourcePage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import { react2angular } from "react2angular";
|
||||
import { isEmpty, get } from "lodash";
|
||||
import Destination, { IMG_ROOT } from "@/services/destination";
|
||||
import { policy } from "@/services/policy";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { $route } from "@/services/ng";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import CardsList from "@/components/cards-list/CardsList";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import CreateSourceDialog from "@/components/CreateSourceDialog";
|
||||
import helper from "@/components/dynamic-form/dynamicFormHelper";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
class DestinationsList extends React.Component {
|
||||
static propTypes = {
|
||||
isNewDestinationPage: PropTypes.bool,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isNewDestinationPage: false,
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
state = {
|
||||
destinationTypes: [],
|
||||
destinations: [],
|
||||
@@ -21,25 +32,27 @@ class DestinationsList extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Promise.all([Destination.query(), Destination.types()]).then(values =>
|
||||
this.setState(
|
||||
{
|
||||
destinations: values[0],
|
||||
destinationTypes: values[1],
|
||||
loading: false,
|
||||
},
|
||||
() => {
|
||||
// all resources are loaded in state
|
||||
if ($route.current.locals.isNewDestinationPage) {
|
||||
if (policy.canCreateDestination()) {
|
||||
this.showCreateSourceDialog();
|
||||
} else {
|
||||
navigateTo("/destinations");
|
||||
Promise.all([Destination.query(), Destination.types()])
|
||||
.then(values =>
|
||||
this.setState(
|
||||
{
|
||||
destinations: values[0],
|
||||
destinationTypes: values[1],
|
||||
loading: false,
|
||||
},
|
||||
() => {
|
||||
// all resources are loaded in state
|
||||
if (this.props.isNewDestinationPage) {
|
||||
if (policy.canCreateDestination()) {
|
||||
this.showCreateSourceDialog();
|
||||
} else {
|
||||
navigateTo("destinations", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
.catch(error => this.props.onError(new PromiseRejectionError(error)));
|
||||
}
|
||||
|
||||
createDestination = (selectedType, values) => {
|
||||
@@ -66,11 +79,15 @@ class DestinationsList extends React.Component {
|
||||
sourceType: "Alert Destination",
|
||||
imageFolder: IMG_ROOT,
|
||||
onCreate: this.createDestination,
|
||||
}).result.then((result = {}) => {
|
||||
if (result.success) {
|
||||
navigateTo(`destinations/${result.data.id}`);
|
||||
}
|
||||
});
|
||||
})
|
||||
.result.then((result = {}) => {
|
||||
if (result.success) {
|
||||
navigateTo(`destinations/${result.data.id}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
navigateTo("destinations", true);
|
||||
});
|
||||
};
|
||||
|
||||
renderDestinations() {
|
||||
@@ -119,45 +136,39 @@ class DestinationsList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageDestinationsList",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
permission: "admin",
|
||||
title: "Alert Destinations",
|
||||
path: "destinations",
|
||||
order: 4,
|
||||
},
|
||||
DestinationsList
|
||||
)
|
||||
)
|
||||
);
|
||||
const DestinationsListPage = wrapSettingsTab(
|
||||
{
|
||||
permission: "admin",
|
||||
title: "Alert Destinations",
|
||||
path: "destinations",
|
||||
order: 4,
|
||||
},
|
||||
DestinationsList
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/destinations",
|
||||
title: "Alert Destinations",
|
||||
key: "destinations",
|
||||
},
|
||||
{
|
||||
path: "/destinations/new",
|
||||
title: "Alert Destinations",
|
||||
key: "destinations",
|
||||
isNewDestinationPage: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
template: "<page-destinations-list></page-destinations-list>",
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/destinations",
|
||||
title: "Alert Destinations",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <DestinationsListPage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/destinations/new",
|
||||
title: "Alert Destinations",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<DestinationsListPage {...currentRoute.routeParams} isNewDestinationPage onError={handleError} />
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { get, find } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Destination, { IMG_ROOT } from "@/services/destination";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { $route } from "@/services/ng";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import notification from "@/services/notification";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import DynamicForm from "@/components/dynamic-form/DynamicForm";
|
||||
import helper from "@/components/dynamic-form/dynamicFormHelper";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
class EditDestination extends React.Component {
|
||||
static propTypes = {
|
||||
destinationId: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -29,18 +30,14 @@ class EditDestination extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Destination.get({ id: $route.current.params.destinationId })
|
||||
Destination.get({ id: this.props.destinationId })
|
||||
.then(destination => {
|
||||
const { type } = destination;
|
||||
this.setState({ destination });
|
||||
Destination.types().then(types => this.setState({ type: find(types, { type }), loading: false }));
|
||||
})
|
||||
.catch(error => {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
error = new PromiseRejectionError(error);
|
||||
}
|
||||
this.props.onError(error);
|
||||
this.props.onError(new PromiseRejectionError(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,7 +59,7 @@ class EditDestination extends React.Component {
|
||||
Destination.delete(destination)
|
||||
.then(() => {
|
||||
notification.success("Alert destination deleted successfully.");
|
||||
navigateTo("/destinations", true);
|
||||
navigateTo("destinations");
|
||||
})
|
||||
.catch(() => {
|
||||
callback();
|
||||
@@ -110,20 +107,16 @@ class EditDestination extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("pageEditDestination", react2angular(wrapSettingsTab(null, EditDestination)));
|
||||
const EditDestinationPage = wrapSettingsTab(null, EditDestination);
|
||||
|
||||
return {
|
||||
"/destinations/:destinationId": {
|
||||
template: '<page-edit-destination on-error="handleError"></page-edit-destination>',
|
||||
title: "Alert Destinations",
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/destinations/:destinationId([0-9]+)",
|
||||
title: "Alert Destinations",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <EditDestinationPage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { filter, map, includes } from "lodash";
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
import Button from "antd/lib/button";
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Icon from "antd/lib/icon";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import Paginator from "@/components/Paginator";
|
||||
|
||||
import { wrap as liveItemsList, ControllerType } from "@/components/items-list/ItemsList";
|
||||
@@ -22,13 +23,12 @@ import ListItemAddon from "@/components/groups/ListItemAddon";
|
||||
import Sidebar from "@/components/groups/DetailsPageSidebar";
|
||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import notification from "@/services/notification";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import Group from "@/services/group";
|
||||
import DataSource from "@/services/data-source";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
class GroupDataSources extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -167,9 +167,11 @@ class GroupDataSources extends React.Component {
|
||||
const promises = map(items, ds => Group.addDataSource({ id: this.groupId }, { data_source_id: ds.id }));
|
||||
return Promise.all(promises);
|
||||
},
|
||||
}).result.finally(() => {
|
||||
this.props.controller.update();
|
||||
});
|
||||
})
|
||||
.result.catch(() => {}) // ignore dismiss
|
||||
.finally(() => {
|
||||
this.props.controller.update();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -185,7 +187,7 @@ class GroupDataSources extends React.Component {
|
||||
items={this.sidebarMenu}
|
||||
canAddDataSources={currentUser.isAdmin}
|
||||
onAddDataSourcesClick={this.addDataSources}
|
||||
onGroupDeleted={() => navigateTo("/groups", true)}
|
||||
onGroupDeleted={() => navigateTo("groups")}
|
||||
/>
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
@@ -227,47 +229,38 @@ class GroupDataSources extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageGroupDataSources",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
null,
|
||||
liveItemsList(
|
||||
GroupDataSources,
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest(unused, { params: { groupId } }) {
|
||||
return { id: groupId };
|
||||
},
|
||||
getResource() {
|
||||
return Group.dataSources.bind(Group);
|
||||
},
|
||||
}),
|
||||
new StateStorage({ orderByField: "name" })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const GroupDataSourcesPage = wrapSettingsTab(
|
||||
null,
|
||||
liveItemsList(
|
||||
GroupDataSources,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest(unused, { params: { groupId } }) {
|
||||
return { id: groupId };
|
||||
},
|
||||
getResource() {
|
||||
return Group.dataSources.bind(Group);
|
||||
},
|
||||
}),
|
||||
() => new StateStorage({ orderByField: "name" })
|
||||
)
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/groups/:groupId/data_sources",
|
||||
title: "Group Data Sources",
|
||||
key: "datasources",
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-group-data-sources on-error="handleError"></page-group-data-sources>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/groups/:groupId([0-9]+)/data_sources",
|
||||
title: "Group Data Sources",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<GroupDataSourcesPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "datasources" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { includes, map } from "lodash";
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
import Button from "antd/lib/button";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import Paginator from "@/components/Paginator";
|
||||
|
||||
import { wrap as liveItemsList, ControllerType } from "@/components/items-list/ItemsList";
|
||||
@@ -19,13 +20,12 @@ import ListItemAddon from "@/components/groups/ListItemAddon";
|
||||
import Sidebar from "@/components/groups/DetailsPageSidebar";
|
||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import notification from "@/services/notification";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import Group from "@/services/group";
|
||||
import User from "@/services/user";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
class GroupMembers extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -130,9 +130,11 @@ class GroupMembers extends React.Component {
|
||||
const promises = map(items, u => Group.addMember({ id: this.groupId }, { user_id: u.id }));
|
||||
return Promise.all(promises);
|
||||
},
|
||||
}).result.finally(() => {
|
||||
this.props.controller.update();
|
||||
});
|
||||
})
|
||||
.result.catch(() => {}) // ignore dismiss
|
||||
.finally(() => {
|
||||
this.props.controller.update();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -148,7 +150,7 @@ class GroupMembers extends React.Component {
|
||||
items={this.sidebarMenu}
|
||||
canAddMembers={currentUser.isAdmin}
|
||||
onAddMembersClick={this.addMembers}
|
||||
onGroupDeleted={() => navigateTo("/groups", true)}
|
||||
onGroupDeleted={() => navigateTo("groups")}
|
||||
/>
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
@@ -190,47 +192,38 @@ class GroupMembers extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageGroupMembers",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
null,
|
||||
liveItemsList(
|
||||
GroupMembers,
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest(unused, { params: { groupId } }) {
|
||||
return { id: groupId };
|
||||
},
|
||||
getResource() {
|
||||
return Group.members.bind(Group);
|
||||
},
|
||||
}),
|
||||
new StateStorage({ orderByField: "name" })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const GroupMembersPage = wrapSettingsTab(
|
||||
null,
|
||||
liveItemsList(
|
||||
GroupMembers,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest(unused, { params: { groupId } }) {
|
||||
return { id: groupId };
|
||||
},
|
||||
getResource() {
|
||||
return Group.members.bind(Group);
|
||||
},
|
||||
}),
|
||||
() => new StateStorage({ orderByField: "name" })
|
||||
)
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/groups/:groupId",
|
||||
title: "Group Members",
|
||||
key: "users",
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-group-members on-error="handleError"></page-group-members>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/groups/:groupId([0-9]+)",
|
||||
title: "Group Members",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<GroupMembersPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "users" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import Button from "antd/lib/button";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import Paginator from "@/components/Paginator";
|
||||
|
||||
import { wrap as liveItemsList, ControllerType } from "@/components/items-list/ItemsList";
|
||||
@@ -15,11 +16,10 @@ import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTab
|
||||
import CreateGroupDialog from "@/components/groups/CreateGroupDialog";
|
||||
import DeleteGroupButton from "@/components/groups/DeleteGroupButton";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import Group from "@/services/group";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
class GroupsList extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -74,9 +74,11 @@ class GroupsList extends React.Component {
|
||||
];
|
||||
|
||||
createGroup = () => {
|
||||
CreateGroupDialog.showModal().result.then(group => {
|
||||
Group.create(group).then(newGroup => navigateTo(`/groups/${newGroup.id}`));
|
||||
});
|
||||
CreateGroupDialog.showModal()
|
||||
.result.then(group => {
|
||||
Group.create(group).then(newGroup => navigateTo(`groups/${newGroup.id}`));
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
};
|
||||
|
||||
onGroupDeleted = () => {
|
||||
@@ -124,52 +126,43 @@ class GroupsList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageGroupsList",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
permission: "list_users",
|
||||
title: "Groups",
|
||||
path: "groups",
|
||||
order: 3,
|
||||
const GroupsListPage = wrapSettingsTab(
|
||||
{
|
||||
permission: "list_users",
|
||||
title: "Groups",
|
||||
path: "groups",
|
||||
order: 3,
|
||||
},
|
||||
liveItemsList(
|
||||
GroupsList,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest() {
|
||||
return {};
|
||||
},
|
||||
liveItemsList(
|
||||
GroupsList,
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest() {
|
||||
return {};
|
||||
},
|
||||
getResource() {
|
||||
return Group.query.bind(Group);
|
||||
},
|
||||
}),
|
||||
new StateStorage({ orderByField: "name", itemsPerPage: 10 })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
getResource() {
|
||||
return Group.query.bind(Group);
|
||||
},
|
||||
}),
|
||||
() => new StateStorage({ orderByField: "name", itemsPerPage: 10 })
|
||||
)
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/groups",
|
||||
title: "Groups",
|
||||
key: "groups",
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-groups-list on-error="handleError"></page-groups-list>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/groups",
|
||||
title: "Groups",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<GroupsListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "groups" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useEffect, useState } from "react";
|
||||
import { axios } from "@/services/axios";
|
||||
import PropTypes from "prop-types";
|
||||
import { includes, isEmpty } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import Alert from "antd/lib/alert";
|
||||
import Icon from "antd/lib/icon";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import EmptyState from "@/components/empty-state/EmptyState";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import BeaconConsent from "@/components/BeaconConsent";
|
||||
@@ -173,15 +173,12 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("homePage", react2angular(Home));
|
||||
|
||||
return {
|
||||
"/": {
|
||||
template: "<home-page></home-page>",
|
||||
title: "Redash",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/",
|
||||
title: "Redash",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<Home {...currentRoute.routeParams} />
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
53
client/app/pages/index.js
Normal file
53
client/app/pages/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { flatten } from "lodash";
|
||||
|
||||
import adminJobsRoutes from "./admin/Jobs";
|
||||
import adminOutdatedQueriesRoutes from "./admin/OutdatedQueries";
|
||||
import adminSystemStatusRoutes from "./admin/SystemStatus";
|
||||
import alertRoutes from "./alert/Alert";
|
||||
import alertsListRoutes from "./alerts/AlertsList";
|
||||
import dashboardListRoutes from "./dashboards/DashboardList";
|
||||
import dashboardRoutes from "./dashboards/DashboardPage";
|
||||
import publicDashboardRoutes from "./dashboards/PublicDashboardPage";
|
||||
import dataSourcesListRoutes from "./data-sources/DataSourcesList";
|
||||
import editDataSourceRoutes from "./data-sources/EditDataSource";
|
||||
import destinationsListRoutes from "./destinations/DestinationsList";
|
||||
import editDestinationRoutes from "./destinations/EditDestination";
|
||||
import groupsListRoutes from "./groups/GroupsList";
|
||||
import groupsDataSourcesRoutes from "./groups/GroupDataSources";
|
||||
import groupsMembersRoutes from "./groups/GroupMembers";
|
||||
import homeRoutes from "./home/Home";
|
||||
import querySourceRoutes from "./queries/QuerySource";
|
||||
import queryViewRoutes from "./queries/QueryView";
|
||||
import visualizationEmbedRoutes from "./queries/VisualizationEmbed";
|
||||
import queriesListRoutes from "./queries-list/QueriesList";
|
||||
import querySnippetsRoutes from "./query-snippets/QuerySnippetsList";
|
||||
import organizationSettingsRoutes from "./settings/OrganizationSettings";
|
||||
import userProfileRoutes from "./users/UserProfile";
|
||||
import usersListRoutes from "./users/UsersList";
|
||||
|
||||
export default flatten([
|
||||
adminJobsRoutes,
|
||||
adminOutdatedQueriesRoutes,
|
||||
adminSystemStatusRoutes,
|
||||
alertRoutes,
|
||||
alertsListRoutes,
|
||||
dashboardListRoutes,
|
||||
dashboardRoutes,
|
||||
publicDashboardRoutes,
|
||||
dataSourcesListRoutes,
|
||||
editDataSourceRoutes,
|
||||
destinationsListRoutes,
|
||||
editDestinationRoutes,
|
||||
groupsListRoutes,
|
||||
groupsDataSourcesRoutes,
|
||||
groupsMembersRoutes,
|
||||
homeRoutes,
|
||||
queriesListRoutes,
|
||||
queryViewRoutes,
|
||||
querySourceRoutes,
|
||||
visualizationEmbedRoutes,
|
||||
querySnippetsRoutes,
|
||||
organizationSettingsRoutes,
|
||||
usersListRoutes,
|
||||
userProfileRoutes,
|
||||
]);
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import Paginator from "@/components/Paginator";
|
||||
import { QueryTagsControl } from "@/components/tags-control/TagsControl";
|
||||
@@ -15,10 +15,10 @@ import * as Sidebar from "@/components/items-list/components/Sidebar";
|
||||
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
|
||||
|
||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import { Query } from "@/services/query";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
import QueriesListEmptyState from "./QueriesListEmptyState";
|
||||
|
||||
@@ -91,114 +91,145 @@ class QueriesList extends React.Component {
|
||||
render() {
|
||||
const { controller } = this.props;
|
||||
return (
|
||||
<div className="container">
|
||||
<PageHeader title={controller.params.title} />
|
||||
<Layout className="m-l-15 m-r-15">
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput
|
||||
placeholder="Search Queries..."
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
/>
|
||||
<Sidebar.Menu items={this.sidebarMenu} selected={controller.params.currentPage} />
|
||||
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} />
|
||||
<Sidebar.PageSizeSelect
|
||||
className="m-b-10"
|
||||
options={controller.pageSizeOptions}
|
||||
value={controller.itemsPerPage}
|
||||
onChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
||||
/>
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
{!controller.isLoaded && <LoadingState />}
|
||||
{controller.isLoaded && controller.isEmpty && (
|
||||
<QueriesListEmptyState
|
||||
page={controller.params.currentPage}
|
||||
searchTerm={controller.searchTerm}
|
||||
selectedTags={controller.selectedTags}
|
||||
<div className="page-queries-list">
|
||||
<div className="container">
|
||||
<PageHeader title={controller.params.title} />
|
||||
<Layout className="m-l-15 m-r-15">
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput
|
||||
placeholder="Search Queries..."
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
/>
|
||||
)}
|
||||
{controller.isLoaded && !controller.isEmpty && (
|
||||
<div className="bg-white tiled table-responsive">
|
||||
<ItemsTable
|
||||
items={controller.pageItems}
|
||||
columns={this.listColumns}
|
||||
orderByField={controller.orderByField}
|
||||
orderByReverse={controller.orderByReverse}
|
||||
toggleSorting={controller.toggleSorting}
|
||||
<Sidebar.Menu items={this.sidebarMenu} selected={controller.params.currentPage} />
|
||||
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} />
|
||||
<Sidebar.PageSizeSelect
|
||||
className="m-b-10"
|
||||
options={controller.pageSizeOptions}
|
||||
value={controller.itemsPerPage}
|
||||
onChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
||||
/>
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
{!controller.isLoaded && <LoadingState />}
|
||||
{controller.isLoaded && controller.isEmpty && (
|
||||
<QueriesListEmptyState
|
||||
page={controller.params.currentPage}
|
||||
searchTerm={controller.searchTerm}
|
||||
selectedTags={controller.selectedTags}
|
||||
/>
|
||||
<Paginator
|
||||
totalCount={controller.totalItemsCount}
|
||||
itemsPerPage={controller.itemsPerPage}
|
||||
page={controller.page}
|
||||
onChange={page => controller.updatePagination({ page })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
)}
|
||||
{controller.isLoaded && !controller.isEmpty && (
|
||||
<div className="bg-white tiled table-responsive">
|
||||
<ItemsTable
|
||||
items={controller.pageItems}
|
||||
columns={this.listColumns}
|
||||
orderByField={controller.orderByField}
|
||||
orderByReverse={controller.orderByReverse}
|
||||
toggleSorting={controller.toggleSorting}
|
||||
/>
|
||||
<Paginator
|
||||
totalCount={controller.totalItemsCount}
|
||||
itemsPerPage={controller.itemsPerPage}
|
||||
page={controller.page}
|
||||
onChange={page => controller.updatePagination({ page })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageQueriesList",
|
||||
react2angular(
|
||||
itemsList(
|
||||
QueriesList,
|
||||
new ResourceItemsSource({
|
||||
getResource({ params: { currentPage } }) {
|
||||
return {
|
||||
all: Query.query.bind(Query),
|
||||
my: Query.myQueries.bind(Query),
|
||||
favorites: Query.favorites.bind(Query),
|
||||
archive: Query.archive.bind(Query),
|
||||
}[currentPage];
|
||||
},
|
||||
getItemProcessor() {
|
||||
return item => new Query(item);
|
||||
},
|
||||
}),
|
||||
new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
)
|
||||
)
|
||||
);
|
||||
const QueriesListPage = itemsList(
|
||||
QueriesList,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
getResource({ params: { currentPage } }) {
|
||||
return {
|
||||
all: Query.query.bind(Query),
|
||||
my: Query.myQueries.bind(Query),
|
||||
favorites: Query.favorites.bind(Query),
|
||||
archive: Query.archive.bind(Query),
|
||||
}[currentPage];
|
||||
},
|
||||
getItemProcessor() {
|
||||
return item => new Query(item);
|
||||
},
|
||||
}),
|
||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/queries",
|
||||
title: "Queries",
|
||||
key: "all",
|
||||
},
|
||||
{
|
||||
path: "/queries/favorites",
|
||||
title: "Favorite Queries",
|
||||
key: "favorites",
|
||||
},
|
||||
{
|
||||
path: "/queries/archive",
|
||||
title: "Archived Queries",
|
||||
key: "archive",
|
||||
},
|
||||
{
|
||||
path: "/queries/my",
|
||||
title: "My Queries",
|
||||
key: "my",
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-queries-list on-error="handleError"></page-queries-list>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/queries",
|
||||
title: "Queries",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<QueriesListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "all" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/queries/favorites",
|
||||
title: "Favorite Queries",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<QueriesListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "favorites" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/queries/archive",
|
||||
title: "Archived Queries",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<QueriesListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "archive" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/queries/my",
|
||||
title: "My Queries",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<QueriesListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "my" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isEmpty, find, map, extend, includes } from "lodash";
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import Select from "antd/lib/select";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import Resizable from "@/components/Resizable";
|
||||
import Parameters from "@/components/Parameters";
|
||||
import EditInPlace from "@/components/EditInPlace";
|
||||
@@ -11,7 +11,7 @@ import EditVisualizationButton from "@/components/EditVisualizationButton";
|
||||
import QueryControlDropdown from "@/components/EditVisualizationButton/QueryControlDropdown";
|
||||
import QueryEditor from "@/components/queries/QueryEditor";
|
||||
import TimeAgo from "@/components/TimeAgo";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { durationHumanize, prettySize } from "@/lib/utils";
|
||||
import { Query } from "@/services/query";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
@@ -443,45 +443,31 @@ QuerySource.propTypes = {
|
||||
query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("pageQuerySource", react2angular(QuerySource));
|
||||
|
||||
return {
|
||||
...routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/queries/new",
|
||||
},
|
||||
],
|
||||
{
|
||||
layout: "fixed",
|
||||
reloadOnSearch: false,
|
||||
template: '<page-query-source ng-if="$resolve.query" query="$resolve.query"></page-query-source>',
|
||||
resolve: {
|
||||
query: () => Query.newQuery(),
|
||||
},
|
||||
}
|
||||
export default [
|
||||
{
|
||||
path: "/queries/new",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key} bodyClass="fixed-layout">
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <QuerySource {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
...routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/queries/:queryId/source",
|
||||
},
|
||||
],
|
||||
{
|
||||
layout: "fixed",
|
||||
reloadOnSearch: false,
|
||||
template: '<page-query-source ng-if="$resolve.query" query="$resolve.query"></page-query-source>',
|
||||
resolve: {
|
||||
query: $route => {
|
||||
"ngInject";
|
||||
|
||||
return Query.get({ id: $route.current.params.queryId });
|
||||
},
|
||||
},
|
||||
}
|
||||
resolve: {
|
||||
query: () => Query.newQuery(),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/queries/:queryId([0-9]+)/source",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key} bodyClass="fixed-layout">
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <QuerySource {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
resolve: {
|
||||
query: ({ queryId }) => Query.get({ id: queryId }),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
page-query-source {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.query-fullscreen {
|
||||
.query-editor-wrapper {
|
||||
padding: 15px;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import Divider from "antd/lib/divider";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import EditInPlace from "@/components/EditInPlace";
|
||||
import Parameters from "@/components/Parameters";
|
||||
import TimeAgo from "@/components/TimeAgo";
|
||||
import QueryControlDropdown from "@/components/EditVisualizationButton/QueryControlDropdown";
|
||||
import EditVisualizationButton from "@/components/EditVisualizationButton";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import DataSource from "@/services/data-source";
|
||||
import { Query } from "@/services/query";
|
||||
import DataSource from "@/services/data-source";
|
||||
import { pluralize, durationHumanize } from "@/lib/utils";
|
||||
|
||||
import QueryPageHeader from "./components/QueryPageHeader";
|
||||
@@ -184,22 +185,16 @@ function QueryView(props) {
|
||||
|
||||
QueryView.propTypes = { query: PropTypes.object.isRequired }; // eslint-disable-line react/forbid-prop-types
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("pageQueryView", react2angular(QueryView));
|
||||
|
||||
return {
|
||||
"/queries/:queryId": {
|
||||
template: '<page-query-view query="$resolve.query"></page-query-view>',
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
query: $route => {
|
||||
"ngInject";
|
||||
|
||||
return Query.get({ id: $route.current.params.queryId });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/queries/:queryId([0-9]+)",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <QueryView {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
resolve: {
|
||||
query: ({ queryId }) => Query.get({ id: queryId }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { find, has } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import moment from "moment";
|
||||
import { markdown } from "markdown";
|
||||
import Button from "antd/lib/button";
|
||||
@@ -9,7 +8,9 @@ import Dropdown from "antd/lib/dropdown";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import { $location, $routeParams } from "@/services/ng";
|
||||
import SignedOutPageWrapper from "@/components/ApplicationArea/SignedOutPageWrapper";
|
||||
import { Query } from "@/services/query";
|
||||
import location from "@/services/location";
|
||||
import { formatDateTime } from "@/lib/utils";
|
||||
import HtmlContent from "@/components/HtmlContent";
|
||||
import Parameters from "@/components/Parameters";
|
||||
@@ -17,12 +18,13 @@ import { Moment } from "@/components/proptypes";
|
||||
import TimeAgo from "@/components/TimeAgo";
|
||||
import Timer from "@/components/Timer";
|
||||
import QueryResultsLink from "@/components/EditVisualizationButton/QueryResultsLink";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import VisualizationName from "@/visualizations/VisualizationName";
|
||||
import VisualizationRenderer from "@/visualizations/VisualizationRenderer";
|
||||
import { VisualizationType } from "@/visualizations";
|
||||
import { Query } from "@/services/query";
|
||||
|
||||
import logoUrl from "@/assets/images/redash_icon_small.png";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
function VisualizationEmbedHeader({ queryName, queryDescription, visualization }) {
|
||||
return (
|
||||
@@ -48,7 +50,15 @@ VisualizationEmbedHeader.propTypes = {
|
||||
|
||||
VisualizationEmbedHeader.defaultProps = { queryDescription: "" };
|
||||
|
||||
function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStartedAt, queryUrl, hideTimestamp }) {
|
||||
function VisualizationEmbedFooter({
|
||||
query,
|
||||
queryResults,
|
||||
updatedAt,
|
||||
refreshStartedAt,
|
||||
queryUrl,
|
||||
hideTimestamp,
|
||||
apiKey,
|
||||
}) {
|
||||
const downloadMenu = (
|
||||
<Menu>
|
||||
<Menu.Item>
|
||||
@@ -56,7 +66,7 @@ function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStart
|
||||
fileType="csv"
|
||||
query={query}
|
||||
queryResult={queryResults}
|
||||
apiKey={$routeParams.api_key}
|
||||
apiKey={apiKey}
|
||||
disabled={!queryResults || !queryResults.getData || !queryResults.getData()}
|
||||
embed>
|
||||
<Icon type="file" /> Download as CSV File
|
||||
@@ -67,7 +77,7 @@ function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStart
|
||||
fileType="tsv"
|
||||
query={query}
|
||||
queryResult={queryResults}
|
||||
apiKey={$routeParams.api_key}
|
||||
apiKey={apiKey}
|
||||
disabled={!queryResults || !queryResults.getData || !queryResults.getData()}
|
||||
embed>
|
||||
<Icon type="file" /> Download as TSV File
|
||||
@@ -78,7 +88,7 @@ function VisualizationEmbedFooter({ query, queryResults, updatedAt, refreshStart
|
||||
fileType="xlsx"
|
||||
query={query}
|
||||
queryResult={queryResults}
|
||||
apiKey={$routeParams.api_key}
|
||||
apiKey={apiKey}
|
||||
disabled={!queryResults || !queryResults.getData || !queryResults.getData()}
|
||||
embed>
|
||||
<Icon type="file-excel" /> Download as Excel File
|
||||
@@ -128,6 +138,7 @@ VisualizationEmbedFooter.propTypes = {
|
||||
refreshStartedAt: Moment,
|
||||
queryUrl: PropTypes.string,
|
||||
hideTimestamp: PropTypes.bool,
|
||||
apiKey: PropTypes.string,
|
||||
};
|
||||
|
||||
VisualizationEmbedFooter.defaultProps = {
|
||||
@@ -136,33 +147,47 @@ VisualizationEmbedFooter.defaultProps = {
|
||||
refreshStartedAt: null,
|
||||
queryUrl: null,
|
||||
hideTimestamp: false,
|
||||
apiKey: null,
|
||||
};
|
||||
|
||||
function VisualizationEmbed({ query }) {
|
||||
function VisualizationEmbed({ queryId, visualizationId, apiKey, onError }) {
|
||||
const [query, setQuery] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [refreshStartedAt, setRefreshStartedAt] = useState(null);
|
||||
const [queryResults, setQueryResults] = useState(null);
|
||||
const hideHeader = has($location.search(), "hide_header");
|
||||
const hideParametersUI = has($location.search(), "hide_parameters");
|
||||
const hideQueryLink = has($location.search(), "hide_link");
|
||||
const hideTimestamp = has($location.search(), "hide_timestamp");
|
||||
|
||||
const showQueryDescription = has($location.search(), "showDescription");
|
||||
const visualizationId = parseInt($routeParams.visualizationId, 10);
|
||||
const visualization = find(query.visualizations, vis => vis.id === visualizationId);
|
||||
const onErrorRef = useRef();
|
||||
onErrorRef.current = onError;
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
Query.get({ id: queryId })
|
||||
.then(result => {
|
||||
if (!isCancelled) {
|
||||
setQuery(result);
|
||||
}
|
||||
})
|
||||
.catch(error => onErrorRef.current(new PromiseRejectionError(error)));
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [queryId]);
|
||||
|
||||
const refreshQueryResults = useCallback(() => {
|
||||
setError(null);
|
||||
setRefreshStartedAt(moment());
|
||||
query
|
||||
.getQueryResultPromise()
|
||||
.then(result => {
|
||||
setQueryResults(result);
|
||||
})
|
||||
.catch(err => {
|
||||
setError(err.getError());
|
||||
})
|
||||
.finally(() => setRefreshStartedAt(null));
|
||||
if (query) {
|
||||
setError(null);
|
||||
setRefreshStartedAt(moment());
|
||||
query
|
||||
.getQueryResultPromise()
|
||||
.then(result => {
|
||||
setQueryResults(result);
|
||||
})
|
||||
.catch(err => {
|
||||
setError(err.getError());
|
||||
})
|
||||
.finally(() => setRefreshStartedAt(null));
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -170,6 +195,27 @@ function VisualizationEmbed({ query }) {
|
||||
refreshQueryResults();
|
||||
}, [refreshQueryResults]);
|
||||
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hideHeader = has(location.search, "hide_header");
|
||||
const hideParametersUI = has(location.search, "hide_parameters");
|
||||
const hideQueryLink = has(location.search, "hide_link");
|
||||
const hideTimestamp = has(location.search, "hide_timestamp");
|
||||
|
||||
const showQueryDescription = has(location.search, "showDescription");
|
||||
visualizationId = parseInt(visualizationId, 10);
|
||||
const visualization = find(query.visualizations, vis => vis.id === visualizationId);
|
||||
|
||||
if (!visualization) {
|
||||
// call error handler async, otherwise it will destroy the component on render phase
|
||||
setTimeout(() => {
|
||||
onError(new Error("Visualization does not exist"));
|
||||
}, 10);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tile m-l-10 m-r-10 p-t-10 embed__vis" data-test="VisualizationEmbed">
|
||||
{!hideHeader && (
|
||||
@@ -204,38 +250,33 @@ function VisualizationEmbed({ query }) {
|
||||
refreshStartedAt={refreshStartedAt}
|
||||
queryUrl={!hideQueryLink ? query.getUrl() : null}
|
||||
hideTimestamp={hideTimestamp}
|
||||
apiKey={apiKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
VisualizationEmbed.propTypes = { query: PropTypes.object.isRequired }; // eslint-disable-line react/forbid-prop-types
|
||||
VisualizationEmbed.propTypes = {
|
||||
queryId: PropTypes.string.isRequired,
|
||||
visualizationId: PropTypes.string,
|
||||
apiKey: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component("visualizationEmbed", react2angular(VisualizationEmbed));
|
||||
VisualizationEmbed.defaultProps = {
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
function loadSession($route, Auth) {
|
||||
const apiKey = $route.current.params.api_key;
|
||||
Auth.setApiKey(apiKey);
|
||||
return Auth.loadConfig();
|
||||
}
|
||||
|
||||
function loadQuery($route, Auth) {
|
||||
"ngInject";
|
||||
|
||||
return loadSession($route, Auth).then(() => Query.get({ id: $route.current.params.queryId }));
|
||||
}
|
||||
|
||||
return {
|
||||
"/embed/query/:queryId/visualization/:visualizationId": {
|
||||
authenticated: false,
|
||||
template: '<visualization-embed query="$resolve.query"></visualization-embed>',
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
query: loadQuery,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/embed/query/:queryId/visualization/:visualizationId",
|
||||
authenticated: false,
|
||||
render: currentRoute => (
|
||||
<SignedOutPageWrapper key={currentRoute.key} apiKey={location.search.api_key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<VisualizationEmbed {...currentRoute.routeParams} apiKey={location.search.api_key} onError={handleError} />
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</SignedOutPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -15,10 +15,12 @@ export default function useAddNewParameterDialog(query, onParameterAdded) {
|
||||
value: null,
|
||||
},
|
||||
existingParams: map(query.getParameters().get(), p => p.name),
|
||||
}).result.then(param => {
|
||||
const newQuery = query.clone();
|
||||
param = newQuery.getParameters().add(param);
|
||||
onParameterAddedRef.current(newQuery, param);
|
||||
});
|
||||
})
|
||||
.result.then(param => {
|
||||
const newQuery = query.clone();
|
||||
param = newQuery.getParameters().add(param);
|
||||
onParameterAddedRef.current(newQuery, param);
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function useAddToDashboardDialog(query) {
|
||||
return useCallback(
|
||||
visualizationId => {
|
||||
const visualization = find(query.visualizations, { id: visualizationId });
|
||||
AddToDashboardDialog.showModal({ visualization });
|
||||
AddToDashboardDialog.showModal({ visualization }).result.catch(() => {}); // ignore dismiss
|
||||
},
|
||||
[query.visualizations]
|
||||
);
|
||||
|
||||
@@ -7,8 +7,10 @@ export default function useApiKeyDialog(query, onChange) {
|
||||
onChangeRef.current = isFunction(onChange) ? onChange : () => {};
|
||||
|
||||
return useCallback(() => {
|
||||
ApiKeyDialog.showModal({ query }).result.then(updatedQuery => {
|
||||
onChangeRef.current(updatedQuery);
|
||||
});
|
||||
ApiKeyDialog.showModal({ query })
|
||||
.result.then(updatedQuery => {
|
||||
onChangeRef.current(updatedQuery);
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@ export default function useEditScheduleDialog(query, onChange) {
|
||||
ScheduleDialog.showModal({
|
||||
schedule: query.schedule,
|
||||
refreshOptions,
|
||||
}).result.then(schedule => {
|
||||
recordEvent("edit_schedule", "query", query.id);
|
||||
updateQuery({ schedule });
|
||||
});
|
||||
})
|
||||
.result.then(schedule => {
|
||||
recordEvent("edit_schedule", "query", query.id);
|
||||
updateQuery({ schedule });
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
}, [query.id, query.schedule, queryFlags.canEdit, queryFlags.canSchedule, updateQuery]);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,15 @@ export default function useEditVisualizationDialog(query, queryResult, onChange)
|
||||
query,
|
||||
visualization,
|
||||
queryResult,
|
||||
}).result.then(updatedVisualization => {
|
||||
const filteredVisualizations = filter(query.visualizations, v => v.id !== updatedVisualization.id);
|
||||
onChangeRef.current(
|
||||
extend(query.clone(), { visualizations: [...filteredVisualizations, updatedVisualization] }),
|
||||
updatedVisualization
|
||||
);
|
||||
});
|
||||
})
|
||||
.result.then(updatedVisualization => {
|
||||
const filteredVisualizations = filter(query.visualizations, v => v.id !== updatedVisualization.id);
|
||||
onChangeRef.current(
|
||||
extend(query.clone(), { visualizations: [...filteredVisualizations, updatedVisualization] }),
|
||||
updatedVisualization
|
||||
);
|
||||
})
|
||||
.catch(() => {}); // ignore dismiss
|
||||
},
|
||||
[query, queryResult]
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function useEmbedDialog(query) {
|
||||
return useCallback(
|
||||
(unusedQuery, visualizationId) => {
|
||||
const visualization = find(query.visualizations, { id: visualizationId });
|
||||
EmbedQueryDialog.showModal({ query, visualization });
|
||||
EmbedQueryDialog.showModal({ query, visualization }).result.catch(() => {}); // ignore dismiss
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
@@ -7,6 +7,6 @@ export default function usePermissionsEditorDialog(query) {
|
||||
aclUrl: `api/queries/${query.id}/acl`,
|
||||
context: "query",
|
||||
author: query.user,
|
||||
});
|
||||
}).result.catch(() => {}); // ignore dismiss
|
||||
}, [query.id, query.user]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import useUpdateQuery from "./useUpdateQuery";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
|
||||
export default function useQuery(originalQuery) {
|
||||
const [query, setQuery] = useState(originalQuery);
|
||||
@@ -10,7 +10,7 @@ export default function useQuery(originalQuery) {
|
||||
// It's important to update URL first, and only then update state
|
||||
if (updatedQuery.id !== query.id) {
|
||||
// Don't reload page when saving new query
|
||||
navigateTo(updatedQuery.getSourceLink(), true, false);
|
||||
navigateTo(updatedQuery.getUrl(true), true);
|
||||
}
|
||||
setQuery(updatedQuery);
|
||||
setOriginalQuerySource(updatedQuery.query);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
||||
import { noop, includes } from "lodash";
|
||||
import useQueryResult from "@/lib/hooks/useQueryResult";
|
||||
import { $location } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
|
||||
function getMaxAge() {
|
||||
const maxAge = $location.search().maxAge;
|
||||
const { maxAge } = location.search;
|
||||
return maxAge !== undefined ? maxAge : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { $rootScope } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
|
||||
// TODO: This should be revisited and probably re-implemented when replacing Angular router with sth else
|
||||
export default function useUnsavedChangesAlert(shouldShowAlert = false) {
|
||||
@@ -16,13 +16,9 @@ export default function useUnsavedChangesAlert(shouldShowAlert = false) {
|
||||
return shouldShowAlertRef.current ? unloadMessage : undefined;
|
||||
};
|
||||
|
||||
const unsubscribe = $rootScope.$on("$locationChangeStart", (event, next, current) => {
|
||||
if (next.split("?")[0] === current.split("?")[0] || next.split("#")[0] === current.split("#")[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldShowAlertRef.current && !window.confirm(confirmMessage)) {
|
||||
event.preventDefault();
|
||||
const unsubscribe = location.confirmChange((nextLocation, currentLocation) => {
|
||||
if (shouldShowAlertRef.current && nextLocation.path !== currentLocation.path) {
|
||||
return confirmMessage;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { first, orderBy, find } from "lodash";
|
||||
import { $location, $rootScope } from "@/services/ng";
|
||||
|
||||
function updateUrlHash(...args) {
|
||||
$location.hash(...args);
|
||||
$rootScope.$applyAsync();
|
||||
}
|
||||
import location from "@/services/location";
|
||||
|
||||
export default function useVisualizationTabHandler(visualizations) {
|
||||
const firstVisualization = useMemo(() => first(orderBy(visualizations, ["id"])) || {}, [visualizations]);
|
||||
const [selectedTab, setSelectedTab] = useState(+$location.hash() || firstVisualization.id);
|
||||
const [selectedTab, setSelectedTab] = useState(+location.hash || firstVisualization.id);
|
||||
|
||||
useEffect(() => {
|
||||
const hashValue = selectedTab !== firstVisualization.id ? `${selectedTab}` : null;
|
||||
if ($location.hash() !== hashValue) {
|
||||
updateUrlHash(hashValue);
|
||||
if (location.hash !== hashValue) {
|
||||
location.setHash(hashValue);
|
||||
}
|
||||
|
||||
const unwatch = $rootScope.$watch(
|
||||
() => $location.hash(),
|
||||
() => {
|
||||
if ($location.hash() !== hashValue) {
|
||||
setSelectedTab(+$location.hash() || firstVisualization.id);
|
||||
}
|
||||
const unlisten = location.listen(() => {
|
||||
if (location.hash !== hashValue) {
|
||||
setSelectedTab(+location.hash || firstVisualization.id);
|
||||
}
|
||||
);
|
||||
return unwatch;
|
||||
});
|
||||
return unlisten;
|
||||
}, [firstVisualization.id, selectedTab]);
|
||||
|
||||
// make sure selectedTab is in visualizations
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { get } from "lodash";
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import Button from "antd/lib/button";
|
||||
import Modal from "antd/lib/modal";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import Paginator from "@/components/Paginator";
|
||||
import QuerySnippetDialog from "@/components/query-snippets/QuerySnippetDialog";
|
||||
|
||||
@@ -15,13 +16,12 @@ import { StateStorage } from "@/components/items-list/classes/StateStorage";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import QuerySnippet from "@/services/query-snippet";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import { policy } from "@/services/policy";
|
||||
import notification from "@/services/notification";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import "./QuerySnippetsList.less";
|
||||
|
||||
const canEditQuerySnippet = querySnippet => currentUser.isAdmin || currentUser.id === get(querySnippet, "user.id");
|
||||
@@ -83,17 +83,13 @@ class QuerySnippetsList extends React.Component {
|
||||
if (policy.isCreateQuerySnippetEnabled()) {
|
||||
this.showSnippetDialog();
|
||||
} else {
|
||||
navigateTo("/query_snippets");
|
||||
navigateTo("query_snippets", true);
|
||||
}
|
||||
} else {
|
||||
QuerySnippet.get({ id: querySnippetId })
|
||||
.then(this.showSnippetDialog)
|
||||
.catch((error = {}) => {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
error = new PromiseRejectionError(error);
|
||||
}
|
||||
this.props.controller.handleError(error);
|
||||
this.props.controller.handleError(new PromiseRejectionError(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -126,15 +122,16 @@ class QuerySnippetsList extends React.Component {
|
||||
|
||||
showSnippetDialog = (querySnippet = null) => {
|
||||
const canSave = !querySnippet || canEditQuerySnippet(querySnippet);
|
||||
navigateTo("/query_snippets/" + get(querySnippet, "id", "new"), true, false);
|
||||
navigateTo("query_snippets/" + get(querySnippet, "id", "new"), true);
|
||||
QuerySnippetDialog.showModal({
|
||||
querySnippet,
|
||||
onSubmit: this.saveQuerySnippet,
|
||||
readOnly: !canSave,
|
||||
})
|
||||
.result.then(() => this.props.controller.update())
|
||||
.catch(() => {}) // ignore dismiss
|
||||
.finally(() => {
|
||||
navigateTo("/query_snippets", true, false);
|
||||
navigateTo("query_snippets", true);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -190,58 +187,62 @@ class QuerySnippetsList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageQuerySnippetsList",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
permission: "create_query",
|
||||
title: "Query Snippets",
|
||||
path: "query_snippets",
|
||||
order: 5,
|
||||
const QuerySnippetsListPage = wrapSettingsTab(
|
||||
{
|
||||
permission: "create_query",
|
||||
title: "Query Snippets",
|
||||
path: "query_snippets",
|
||||
order: 5,
|
||||
},
|
||||
liveItemsList(
|
||||
QuerySnippetsList,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest() {
|
||||
return {};
|
||||
},
|
||||
liveItemsList(
|
||||
QuerySnippetsList,
|
||||
new ResourceItemsSource({
|
||||
isPlainList: true,
|
||||
getRequest() {
|
||||
return {};
|
||||
},
|
||||
getResource() {
|
||||
return QuerySnippet.query.bind(QuerySnippet);
|
||||
},
|
||||
}),
|
||||
new StateStorage({ orderByField: "trigger", itemsPerPage: 10 })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
getResource() {
|
||||
return QuerySnippet.query.bind(QuerySnippet);
|
||||
},
|
||||
}),
|
||||
() => new StateStorage({ orderByField: "trigger", itemsPerPage: 10 })
|
||||
)
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/query_snippets",
|
||||
title: "Query Snippets",
|
||||
key: "query_snippets",
|
||||
},
|
||||
{
|
||||
path: "/query_snippets/:querySnippetId",
|
||||
title: "Query Snippets",
|
||||
key: "query_snippets",
|
||||
isNewOrEditPage: true,
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-query-snippets-list on-error="handleError"></page-query-snippets-list>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/query_snippets",
|
||||
title: "Query Snippets",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<QuerySnippetsListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "query_snippets" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/query_snippets/:querySnippetId(new|[0-9]+)",
|
||||
title: "Query Snippets",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<QuerySnippetsListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "query_snippets", isNewOrEditPage: true }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
import PropTypes from "prop-types";
|
||||
import { isEmpty, join, get } from "lodash";
|
||||
|
||||
import Alert from "antd/lib/alert";
|
||||
@@ -9,19 +9,29 @@ import Input from "antd/lib/input";
|
||||
import Select from "antd/lib/select";
|
||||
import Checkbox from "antd/lib/checkbox";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import OrgSettings from "@/services/organizationSettings";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
class OrganizationSettings extends React.Component {
|
||||
static propTypes = {
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
state = {
|
||||
settings: {},
|
||||
formValues: {},
|
||||
@@ -31,10 +41,12 @@ class OrganizationSettings extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
recordEvent("view", "page", "org_settings");
|
||||
OrgSettings.get().then(response => {
|
||||
const settings = get(response, "settings");
|
||||
this.setState({ settings, formValues: { ...settings }, loading: false });
|
||||
});
|
||||
OrgSettings.get()
|
||||
.then(response => {
|
||||
const settings = get(response, "settings");
|
||||
this.setState({ settings, formValues: { ...settings }, loading: false });
|
||||
})
|
||||
.catch(error => this.props.onError(new PromiseRejectionError(error)));
|
||||
}
|
||||
|
||||
disablePasswordLoginToggle = () => !(clientConfig.googleLoginEnabled || this.state.formValues.auth_saml_enabled);
|
||||
@@ -48,6 +60,7 @@ class OrganizationSettings extends React.Component {
|
||||
const settings = get(response, "settings");
|
||||
this.setState({ settings, formValues: { ...settings } });
|
||||
})
|
||||
.catch(error => this.props.onError(new PromiseRejectionError(error)))
|
||||
.finally(() => this.setState({ submitting: false }));
|
||||
}
|
||||
};
|
||||
@@ -259,40 +272,24 @@ class OrganizationSettings extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageOrganizationSettings",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
permission: "admin",
|
||||
title: "Settings",
|
||||
path: "settings/organization",
|
||||
order: 6,
|
||||
},
|
||||
OrganizationSettings
|
||||
)
|
||||
)
|
||||
);
|
||||
const OrganizationSettingsPage = wrapSettingsTab(
|
||||
{
|
||||
permission: "admin",
|
||||
title: "Settings",
|
||||
path: "settings/organization",
|
||||
order: 6,
|
||||
},
|
||||
OrganizationSettings
|
||||
);
|
||||
|
||||
return routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/settings/organization",
|
||||
title: "Organization Settings",
|
||||
key: "organization-settings",
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-organization-settings on-error="handleError"></page-organization-settings>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default {
|
||||
path: "/settings/organization",
|
||||
title: "Organization Settings",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <OrganizationSettingsPage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import EmailSettingsWarning from "@/components/EmailSettingsWarning";
|
||||
import UserEdit from "@/components/users/UserEdit";
|
||||
import UserShow from "@/components/users/UserShow";
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import User from "@/services/user";
|
||||
import { $route } from "@/services/ng";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import "./settings.less";
|
||||
|
||||
class UserProfile extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string,
|
||||
onError: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
userId: null, // defaults to `currentUser.id`
|
||||
onError: () => {},
|
||||
};
|
||||
|
||||
@@ -29,15 +31,11 @@ class UserProfile extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const userId = $route.current.params.userId || currentUser.id;
|
||||
const userId = this.props.userId || currentUser.id;
|
||||
User.get({ id: userId })
|
||||
.then(user => this.setState({ user: User.convertUserInfo(user) }))
|
||||
.catch(error => {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
error = new PromiseRejectionError(error);
|
||||
}
|
||||
this.props.onError(error);
|
||||
this.props.onError(new PromiseRejectionError(error));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,20 +52,36 @@ class UserProfile extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageUserProfile",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
title: "Account",
|
||||
path: "users/me",
|
||||
order: 7,
|
||||
},
|
||||
UserProfile
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
const UserProfilePage = wrapSettingsTab(
|
||||
{
|
||||
title: "Account",
|
||||
path: "users/me",
|
||||
order: 7,
|
||||
},
|
||||
UserProfile
|
||||
);
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/users/me",
|
||||
title: "Account",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <UserProfilePage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/users/:userId([0-9]+)",
|
||||
title: "Users",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => <UserProfilePage {...currentRoute.routeParams} onError={handleError} />}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { map, get } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import Button from "antd/lib/button";
|
||||
import Modal from "antd/lib/modal";
|
||||
import AuthenticatedPageWrapper from "@/components/ApplicationArea/AuthenticatedPageWrapper";
|
||||
import Paginator from "@/components/Paginator";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import { UserPreviewCard } from "@/components/PreviewCard";
|
||||
@@ -22,11 +22,12 @@ import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTab
|
||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||
import CreateUserDialog from "@/components/users/CreateUserDialog";
|
||||
import wrapSettingsTab from "@/components/SettingsWrapper";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
|
||||
import { currentUser } from "@/services/auth";
|
||||
import { policy } from "@/services/policy";
|
||||
import User from "@/services/user";
|
||||
import navigateTo from "@/services/navigateTo";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
import notification from "@/services/notification";
|
||||
import { absoluteUrl } from "@/services/utils";
|
||||
|
||||
@@ -168,6 +169,7 @@ class UsersList extends React.Component {
|
||||
if (policy.isCreateUserEnabled()) {
|
||||
CreateUserDialog.showModal({ onCreate: this.createUser })
|
||||
.result.then(() => this.props.controller.update())
|
||||
.catch(() => {}) // ignore dismiss
|
||||
.finally(() => {
|
||||
if (this.props.controller.params.isNewUserPage) {
|
||||
navigateTo("users");
|
||||
@@ -242,45 +244,108 @@ class UsersList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component(
|
||||
"pageUsersList",
|
||||
react2angular(
|
||||
wrapSettingsTab(
|
||||
{
|
||||
permission: "list_users",
|
||||
title: "Users",
|
||||
path: "users",
|
||||
isActive: path => path.startsWith("/users") && path !== "/users/me",
|
||||
order: 2,
|
||||
const UsersListPage = wrapSettingsTab(
|
||||
{
|
||||
permission: "list_users",
|
||||
title: "Users",
|
||||
path: "users",
|
||||
isActive: path => path.startsWith("/users") && path !== "/users/me",
|
||||
order: 2,
|
||||
},
|
||||
itemsList(
|
||||
UsersList,
|
||||
() =>
|
||||
new ResourceItemsSource({
|
||||
getRequest(request, { params: { currentPage } }) {
|
||||
switch (currentPage) {
|
||||
case "active":
|
||||
request.pending = false;
|
||||
break;
|
||||
case "pending":
|
||||
request.pending = true;
|
||||
break;
|
||||
case "disabled":
|
||||
request.disabled = true;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
return request;
|
||||
},
|
||||
itemsList(
|
||||
UsersList,
|
||||
new ResourceItemsSource({
|
||||
getRequest(request, { params: { currentPage } }) {
|
||||
switch (currentPage) {
|
||||
case "active":
|
||||
request.pending = false;
|
||||
break;
|
||||
case "pending":
|
||||
request.pending = true;
|
||||
break;
|
||||
case "disabled":
|
||||
request.disabled = true;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
return request;
|
||||
},
|
||||
getResource() {
|
||||
return User.query.bind(User);
|
||||
},
|
||||
}),
|
||||
new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
getResource() {
|
||||
return User.query.bind(User);
|
||||
},
|
||||
}),
|
||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
||||
)
|
||||
);
|
||||
|
||||
init.init = true;
|
||||
export default [
|
||||
{
|
||||
path: "/users",
|
||||
title: "Users",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<UsersListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "active" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/users/new",
|
||||
title: "Users",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<UsersListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "active", isNewUserPage: true }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/users/pending",
|
||||
title: "Pending Invitations",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<UsersListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "pending" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/users/disabled",
|
||||
title: "Disabled Users",
|
||||
render: currentRoute => (
|
||||
<AuthenticatedPageWrapper key={currentRoute.key}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) => (
|
||||
<UsersListPage
|
||||
routeParams={{ ...currentRoute.routeParams, currentPage: "disabled" }}
|
||||
currentRoute={currentRoute}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</AuthenticatedPageWrapper>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { extend } from "lodash";
|
||||
import { routesToAngularRoutes } from "@/lib/utils";
|
||||
|
||||
export default function init() {
|
||||
const listRoutes = routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/users",
|
||||
title: "Users",
|
||||
key: "active",
|
||||
},
|
||||
{
|
||||
path: "/users/new",
|
||||
title: "Users",
|
||||
key: "active",
|
||||
isNewUserPage: true,
|
||||
},
|
||||
{
|
||||
path: "/users/pending",
|
||||
title: "Pending Invitations",
|
||||
key: "pending",
|
||||
},
|
||||
{
|
||||
path: "/users/disabled",
|
||||
title: "Disabled Users",
|
||||
key: "disabled",
|
||||
},
|
||||
],
|
||||
{
|
||||
template: '<page-users-list on-error="handleError"></page-users-list>',
|
||||
reloadOnSearch: false,
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const profileRoutes = routesToAngularRoutes(
|
||||
[
|
||||
{
|
||||
path: "/users/me",
|
||||
title: "Account",
|
||||
key: "users",
|
||||
},
|
||||
{
|
||||
path: "/users/:userId",
|
||||
title: "Users",
|
||||
key: "users",
|
||||
},
|
||||
],
|
||||
{
|
||||
reloadOnSearch: false,
|
||||
template: '<page-user-profile on-error="handleError"></page-user-profile>',
|
||||
controller($scope, $exceptionHandler) {
|
||||
"ngInject";
|
||||
|
||||
$scope.handleError = $exceptionHandler;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return extend(listRoutes, profileRoutes);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
@@ -1,10 +1,8 @@
|
||||
import debug from "debug";
|
||||
import { includes, extend } from "lodash";
|
||||
import location from "@/services/location";
|
||||
import { axios } from "@/services/axios";
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let Auth = null;
|
||||
|
||||
export const currentUser = {
|
||||
canEdit(object) {
|
||||
const userId = object.user_id || (object.user && object.user.id);
|
||||
@@ -40,78 +38,63 @@ function updateSession(sessionData) {
|
||||
extend(messages, session.messages);
|
||||
}
|
||||
|
||||
function AuthService($window, $location, $q) {
|
||||
return {
|
||||
isAuthenticated() {
|
||||
return session.loaded && session.user.id;
|
||||
},
|
||||
login() {
|
||||
const next = encodeURI($location.url());
|
||||
logger("Calling login with next = %s", next);
|
||||
window.location.href = `login?next=${next}`;
|
||||
},
|
||||
logout() {
|
||||
logger("Logout.");
|
||||
$window.location.href = "logout";
|
||||
},
|
||||
loadSession() {
|
||||
logger("Loading session");
|
||||
if (session.loaded && session.user.id) {
|
||||
logger("Resolving with local value.");
|
||||
return $q.resolve(session);
|
||||
}
|
||||
export const Auth = {
|
||||
isAuthenticated() {
|
||||
return session.loaded && session.user.id;
|
||||
},
|
||||
login() {
|
||||
const next = encodeURI(location.url);
|
||||
logger("Calling login with next = %s", next);
|
||||
window.location.href = `login?next=${next}`;
|
||||
},
|
||||
logout() {
|
||||
logger("Logout.");
|
||||
window.location.href = "logout";
|
||||
},
|
||||
loadSession() {
|
||||
logger("Loading session");
|
||||
if (session.loaded && session.user.id) {
|
||||
logger("Resolving with local value.");
|
||||
return Promise.resolve(session);
|
||||
}
|
||||
|
||||
this.setApiKey(null);
|
||||
return axios.get("api/session").then(data => {
|
||||
updateSession(data);
|
||||
return session;
|
||||
Auth.setApiKey(null);
|
||||
return axios.get("api/session").then(data => {
|
||||
updateSession(data);
|
||||
return session;
|
||||
});
|
||||
},
|
||||
loadConfig() {
|
||||
logger("Loading config");
|
||||
return axios.get("/api/config").then(data => {
|
||||
updateSession({ client_config: data.client_config, user: { permissions: [] }, messages: [] });
|
||||
return data;
|
||||
});
|
||||
},
|
||||
setApiKey(apiKey) {
|
||||
logger("Set API key to: %s", apiKey);
|
||||
Auth.apiKey = apiKey;
|
||||
},
|
||||
getApiKey() {
|
||||
return Auth.apiKey;
|
||||
},
|
||||
requireSession() {
|
||||
logger("Requested authentication");
|
||||
if (Auth.isAuthenticated()) {
|
||||
return Promise.resolve(session);
|
||||
}
|
||||
return Auth.loadSession()
|
||||
.then(() => {
|
||||
if (Auth.isAuthenticated()) {
|
||||
logger("Loaded session");
|
||||
return session;
|
||||
}
|
||||
logger("Need to login, redirecting");
|
||||
Auth.login();
|
||||
})
|
||||
.catch(() => {
|
||||
logger("Need to login, redirecting");
|
||||
Auth.login();
|
||||
});
|
||||
},
|
||||
loadConfig() {
|
||||
logger("Loading config");
|
||||
return axios.get("/api/config").then(data => {
|
||||
updateSession({ client_config: data.client_config, user: { permissions: [] }, messages: [] });
|
||||
return data;
|
||||
});
|
||||
},
|
||||
setApiKey(apiKey) {
|
||||
logger("Set API key to: %s", apiKey);
|
||||
this.apiKey = apiKey;
|
||||
},
|
||||
getApiKey() {
|
||||
return this.apiKey;
|
||||
},
|
||||
requireSession() {
|
||||
logger("Requested authentication");
|
||||
if (this.isAuthenticated()) {
|
||||
return $q.when(session);
|
||||
}
|
||||
return this.loadSession()
|
||||
.then(() => {
|
||||
if (this.isAuthenticated()) {
|
||||
logger("Loaded session");
|
||||
return session;
|
||||
}
|
||||
logger("Need to login, redirecting");
|
||||
this.login();
|
||||
})
|
||||
.catch(() => {
|
||||
logger("Need to login, redirecting");
|
||||
this.login();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.factory("Auth", AuthService);
|
||||
ngModule.value("currentUser", currentUser);
|
||||
ngModule.value("clientConfig", clientConfig);
|
||||
ngModule.value("messages", messages);
|
||||
|
||||
ngModule.run($injector => {
|
||||
Auth = $injector.get("Auth");
|
||||
});
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axiosLib from "axios";
|
||||
import { Auth } from "@/services/auth";
|
||||
import qs from "query-string";
|
||||
|
||||
export const axios = axiosLib.create({
|
||||
@@ -10,18 +11,11 @@ const getResponse = ({ response }) => Promise.reject(response);
|
||||
|
||||
axios.interceptors.response.use(getData, getResponse);
|
||||
|
||||
// TODO: revisit this definition when auth is updated
|
||||
export default function init(ngModule) {
|
||||
ngModule.run($injector => {
|
||||
axios.interceptors.request.use(config => {
|
||||
const apiKey = $injector.get("Auth").getApiKey();
|
||||
if (apiKey) {
|
||||
config.headers.Authorization = `Key ${apiKey}`;
|
||||
}
|
||||
axios.interceptors.request.use(config => {
|
||||
const apiKey = Auth.getApiKey();
|
||||
if (apiKey) {
|
||||
config.headers.Authorization = `Key ${apiKey}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
return config;
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@ import { axios } from "@/services/axios";
|
||||
import dashboardGridOptions from "@/config/dashboard-grid-options";
|
||||
import Widget from "./widget";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import { $location } from "@/services/ng";
|
||||
import location from "@/services/location";
|
||||
import { cloneParameter } from "@/services/parameters";
|
||||
|
||||
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
|
||||
const filters = {};
|
||||
@@ -170,7 +171,7 @@ Dashboard.prototype.canEdit = function canEdit() {
|
||||
|
||||
Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
||||
const globalParams = {};
|
||||
const queryParams = $location.search();
|
||||
const queryParams = location.search;
|
||||
_.each(this.widgets, widget => {
|
||||
if (widget.getQuery()) {
|
||||
const mappings = widget.getParameterMappings();
|
||||
@@ -182,7 +183,7 @@ Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
||||
if (mapping.type === Widget.MappingType.DashboardLevel) {
|
||||
// create global param
|
||||
if (!globalParams[mapping.mapTo]) {
|
||||
globalParams[mapping.mapTo] = param.clone();
|
||||
globalParams[mapping.mapTo] = cloneParameter(param);
|
||||
globalParams[mapping.mapTo].name = mapping.mapTo;
|
||||
globalParams[mapping.mapTo].title = mapping.title || param.title;
|
||||
globalParams[mapping.mapTo].locals = [];
|
||||
|
||||
99
client/app/services/location.js
Normal file
99
client/app/services/location.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { isNil, isUndefined, isFunction, isObject, trimStart, mapValues, omitBy, extend } from "lodash";
|
||||
import qs from "query-string";
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
function normalizeLocation(rawLocation) {
|
||||
const { pathname, search, hash } = rawLocation;
|
||||
const result = {};
|
||||
|
||||
result.path = pathname;
|
||||
result.search = mapValues(qs.parse(search), value => (isNil(value) ? true : value));
|
||||
result.hash = trimStart(hash, "#");
|
||||
result.url = `${pathname}${search}${hash}`;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const location = {
|
||||
listen(handler) {
|
||||
if (isFunction(handler)) {
|
||||
return history.listen((unused, action) => handler(location, action));
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
},
|
||||
|
||||
confirmChange(handler) {
|
||||
if (isFunction(handler)) {
|
||||
return history.block(nextLocation => {
|
||||
return handler(normalizeLocation(nextLocation), location);
|
||||
});
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
},
|
||||
|
||||
update(newLocation, replace = false) {
|
||||
if (isObject(newLocation)) {
|
||||
// remap fields and remove undefined ones
|
||||
newLocation = omitBy(
|
||||
{
|
||||
pathname: newLocation.path,
|
||||
search: newLocation.search,
|
||||
hash: newLocation.hash,
|
||||
},
|
||||
isUndefined
|
||||
);
|
||||
|
||||
// keep existing fields (!)
|
||||
newLocation = extend(
|
||||
{
|
||||
pathname: location.path,
|
||||
search: location.search,
|
||||
hash: location.hash,
|
||||
},
|
||||
newLocation
|
||||
);
|
||||
|
||||
// serialize search and keep existing search parameters (!)
|
||||
if (isObject(newLocation.search)) {
|
||||
newLocation.search = omitBy(extend({}, location.search, newLocation.search), isNil);
|
||||
newLocation.search = mapValues(newLocation.search, value => (value === true ? null : value));
|
||||
newLocation.search = qs.stringify(newLocation.search);
|
||||
}
|
||||
}
|
||||
if (replace) {
|
||||
history.replace(newLocation);
|
||||
} else {
|
||||
history.push(newLocation);
|
||||
}
|
||||
},
|
||||
|
||||
url: undefined,
|
||||
|
||||
path: undefined,
|
||||
setPath(path, replace = false) {
|
||||
location.update({ path }, replace);
|
||||
},
|
||||
|
||||
search: undefined,
|
||||
setSearch(search, replace = false) {
|
||||
location.update({ search }, replace);
|
||||
},
|
||||
|
||||
hash: undefined,
|
||||
setHash(hash, replace = false) {
|
||||
location.update({ hash }, replace);
|
||||
},
|
||||
};
|
||||
|
||||
function locationChanged() {
|
||||
extend(location, normalizeLocation(history.location));
|
||||
}
|
||||
|
||||
history.listen(locationChanged);
|
||||
locationChanged(); // init service
|
||||
|
||||
export default location;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { isString } from "lodash";
|
||||
import { $location, $rootScope, $route } from "@/services/ng";
|
||||
|
||||
export default function navigateTo(url, replace = false, reload = true) {
|
||||
if (isString(url)) {
|
||||
// Allows changing the URL without reloading
|
||||
// ANGULAR_REMOVE_ME Revisit when some React router will be used
|
||||
if (!reload) {
|
||||
const lastRoute = $route.current;
|
||||
const un = $rootScope.$on("$locationChangeSuccess", () => {
|
||||
$route.current = lastRoute;
|
||||
un();
|
||||
});
|
||||
}
|
||||
$location.url(url);
|
||||
if (replace) {
|
||||
$location.replace();
|
||||
}
|
||||
$rootScope.$applyAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export let $http = null; // eslint-disable-line import/no-mutable-exports
|
||||
export let $location = null; // eslint-disable-line import/no-mutable-exports
|
||||
export let $route = null; // eslint-disable-line import/no-mutable-exports
|
||||
export let $routeParams = null; // eslint-disable-line import/no-mutable-exports
|
||||
export let $q = null; // eslint-disable-line import/no-mutable-exports
|
||||
export let $rootScope = null; // eslint-disable-line import/no-mutable-exports
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.run($injector => {
|
||||
$http = $injector.get("$http");
|
||||
$location = $injector.get("$location");
|
||||
$route = $injector.get("$route");
|
||||
$routeParams = $injector.get("$routeParams");
|
||||
$q = $injector.get("$q");
|
||||
$rootScope = $injector.get("$rootScope");
|
||||
});
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
@@ -8,8 +8,8 @@ function addOnlineListener(notificationKey) {
|
||||
window.addEventListener("online", onlineStateHandler);
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.run(() => {
|
||||
export default {
|
||||
init() {
|
||||
window.addEventListener("offline", () => {
|
||||
notification.warning("Please check your Internet connection.", null, {
|
||||
key: "connectionNotification",
|
||||
@@ -17,7 +17,5 @@ export default function init(ngModule) {
|
||||
});
|
||||
addOnlineListener("connectionNotification");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { findKey, startsWith, has, includes, isNull, values } from "lodash";
|
||||
import moment from "moment";
|
||||
import PropTypes from "prop-types";
|
||||
import { Parameter } from ".";
|
||||
import Parameter from "./Parameter";
|
||||
|
||||
const DATETIME_FORMATS = {
|
||||
// eslint-disable-next-line quote-props
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { startsWith, has, includes, findKey, values, isObject, isArray } from "lodash";
|
||||
import moment from "moment";
|
||||
import PropTypes from "prop-types";
|
||||
import { Parameter } from ".";
|
||||
import Parameter from "./Parameter";
|
||||
|
||||
const DATETIME_FORMATS = {
|
||||
"date-range": "YYYY-MM-DD",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isArray, isEmpty, includes, intersection, get, map, join, has } from "lodash";
|
||||
import { Parameter } from ".";
|
||||
import Parameter from "./Parameter";
|
||||
|
||||
class EnumParameter extends Parameter {
|
||||
constructor(parameter, parentQueryId) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toNumber, isNull } from "lodash";
|
||||
import { Parameter } from ".";
|
||||
import Parameter from "./Parameter";
|
||||
|
||||
class NumberParameter extends Parameter {
|
||||
constructor(parameter, parentQueryId) {
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { isNull, isObject, isFunction, isUndefined, isEqual, has, omit, isArray, each } from "lodash";
|
||||
import {
|
||||
TextParameter,
|
||||
NumberParameter,
|
||||
EnumParameter,
|
||||
QueryBasedDropdownParameter,
|
||||
DateParameter,
|
||||
DateRangeParameter,
|
||||
} from ".";
|
||||
|
||||
class Parameter {
|
||||
constructor(parameter, parentQueryId) {
|
||||
@@ -23,27 +15,6 @@ class Parameter {
|
||||
this.urlPrefix = "p_";
|
||||
}
|
||||
|
||||
static create(param, parentQueryId) {
|
||||
switch (param.type) {
|
||||
case "number":
|
||||
return new NumberParameter(param, parentQueryId);
|
||||
case "enum":
|
||||
return new EnumParameter(param, parentQueryId);
|
||||
case "query":
|
||||
return new QueryBasedDropdownParameter(param, parentQueryId);
|
||||
case "date":
|
||||
case "datetime-local":
|
||||
case "datetime-with-seconds":
|
||||
return new DateParameter(param, parentQueryId);
|
||||
case "date-range":
|
||||
case "datetime-range":
|
||||
case "datetime-range-with-seconds":
|
||||
return new DateRangeParameter(param, parentQueryId);
|
||||
default:
|
||||
return new TextParameter({ ...param, type: "text" }, parentQueryId);
|
||||
}
|
||||
}
|
||||
|
||||
static getExecutionValue(param, extra = {}) {
|
||||
if (!isObject(param) || !isFunction(param.getExecutionValue)) {
|
||||
return null;
|
||||
@@ -73,10 +44,6 @@ class Parameter {
|
||||
return this.$$value;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return Parameter.create(this, this.parentQueryId);
|
||||
}
|
||||
|
||||
isEmptyValue(value) {
|
||||
return isNull(this.normalizeValue(value));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isNull, isUndefined, isArray, isEmpty, get, map, join, has } from "lodash";
|
||||
import { Query } from "@/services/query";
|
||||
import { Parameter } from ".";
|
||||
import Parameter from "./Parameter";
|
||||
|
||||
class QueryBasedDropdownParameter extends Parameter {
|
||||
constructor(parameter, parentQueryId) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toString, isEmpty } from "lodash";
|
||||
import { Parameter } from ".";
|
||||
import Parameter from "./Parameter";
|
||||
|
||||
class TextParameter extends Parameter {
|
||||
constructor(parameter, parentQueryId) {
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
export { default as Parameter } from "./Parameter";
|
||||
export { default as TextParameter } from "./TextParameter";
|
||||
export { default as NumberParameter } from "./NumberParameter";
|
||||
export { default as EnumParameter } from "./EnumParameter";
|
||||
export { default as QueryBasedDropdownParameter } from "./QueryBasedDropdownParameter";
|
||||
export { default as DateParameter } from "./DateParameter";
|
||||
export { default as DateRangeParameter } from "./DateRangeParameter";
|
||||
import Parameter from "./Parameter";
|
||||
import TextParameter from "./TextParameter";
|
||||
import NumberParameter from "./NumberParameter";
|
||||
import EnumParameter from "./EnumParameter";
|
||||
import QueryBasedDropdownParameter from "./QueryBasedDropdownParameter";
|
||||
import DateParameter from "./DateParameter";
|
||||
import DateRangeParameter from "./DateRangeParameter";
|
||||
|
||||
function createParameter(param, parentQueryId) {
|
||||
switch (param.type) {
|
||||
case "number":
|
||||
return new NumberParameter(param, parentQueryId);
|
||||
case "enum":
|
||||
return new EnumParameter(param, parentQueryId);
|
||||
case "query":
|
||||
return new QueryBasedDropdownParameter(param, parentQueryId);
|
||||
case "date":
|
||||
case "datetime-local":
|
||||
case "datetime-with-seconds":
|
||||
return new DateParameter(param, parentQueryId);
|
||||
case "date-range":
|
||||
case "datetime-range":
|
||||
case "datetime-range-with-seconds":
|
||||
return new DateRangeParameter(param, parentQueryId);
|
||||
default:
|
||||
return new TextParameter({ ...param, type: "text" }, parentQueryId);
|
||||
}
|
||||
}
|
||||
|
||||
function cloneParameter(param) {
|
||||
return createParameter(param, param.parentQueryId);
|
||||
}
|
||||
|
||||
export {
|
||||
Parameter,
|
||||
TextParameter,
|
||||
NumberParameter,
|
||||
EnumParameter,
|
||||
QueryBasedDropdownParameter,
|
||||
DateParameter,
|
||||
DateRangeParameter,
|
||||
createParameter,
|
||||
cloneParameter,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Parameter } from "..";
|
||||
import { createParameter } from "..";
|
||||
import { getDynamicDateFromString } from "../DateParameter";
|
||||
import moment from "moment";
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("DateParameter", () => {
|
||||
let param;
|
||||
|
||||
beforeEach(() => {
|
||||
param = Parameter.create({ name: "param", title: "Param", type });
|
||||
param = createParameter({ name: "param", title: "Param", type });
|
||||
});
|
||||
|
||||
describe("getExecutionValue", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Parameter } from "..";
|
||||
import { createParameter } from "..";
|
||||
import { getDynamicDateRangeFromString } from "../DateRangeParameter";
|
||||
import moment from "moment";
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("DateRangeParameter", () => {
|
||||
let param;
|
||||
|
||||
beforeEach(() => {
|
||||
param = Parameter.create({ name: "param", title: "Param", type });
|
||||
param = createParameter({ name: "param", title: "Param", type });
|
||||
});
|
||||
|
||||
describe("getExecutionValue", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Parameter } from "..";
|
||||
import { createParameter } from "..";
|
||||
|
||||
describe("EnumParameter", () => {
|
||||
let param;
|
||||
@@ -13,7 +13,7 @@ describe("EnumParameter", () => {
|
||||
enumOptions,
|
||||
multiValuesOptions,
|
||||
};
|
||||
param = Parameter.create(paramOptions);
|
||||
param = createParameter(paramOptions);
|
||||
});
|
||||
|
||||
describe("normalizeValue", () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Parameter } from "..";
|
||||
import { createParameter } from "..";
|
||||
|
||||
describe("NumberParameter", () => {
|
||||
let param;
|
||||
|
||||
beforeEach(() => {
|
||||
param = Parameter.create({ name: "param", title: "Param", type: "number" });
|
||||
param = createParameter({ name: "param", title: "Param", type: "number" });
|
||||
});
|
||||
|
||||
describe("normalizeValue", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
Parameter,
|
||||
createParameter,
|
||||
TextParameter,
|
||||
NumberParameter,
|
||||
EnumParameter,
|
||||
@@ -25,7 +25,7 @@ describe("Parameter", () => {
|
||||
];
|
||||
|
||||
test.each(parameterTypes)("when type is '%s' creates a %p", (type, expectedClass) => {
|
||||
const parameter = Parameter.create({ name: "param", title: "Param", type });
|
||||
const parameter = createParameter({ name: "param", title: "Param", type });
|
||||
expect(parameter).toBeInstanceOf(expectedClass);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Parameter } from "..";
|
||||
import { createParameter } from "..";
|
||||
|
||||
describe("QueryBasedDropdownParameter", () => {
|
||||
let param;
|
||||
@@ -12,7 +12,7 @@ describe("QueryBasedDropdownParameter", () => {
|
||||
queryId: 1,
|
||||
multiValuesOptions,
|
||||
};
|
||||
param = Parameter.create(paramOptions);
|
||||
param = createParameter(paramOptions);
|
||||
});
|
||||
|
||||
describe("normalizeValue", () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user