mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Misc changes to codebase back ported from internal fork (#5129)
* Set corejs version in .babelrc so Jest doesn't complain. * Rewrite services/routes in TypeScript. * Add TypeScript definitions for DialogComponent. * Make image paths more portable * Add current route context and hook. * Make EmptyState more flexible by being able to pass in getSteps function. * Rewrite ItemsList in TypeScript. * Introduce the possibility to add custom sorters for a column. * Rearrange props to be friendly to TypeScript. * Type definitions for NotificationApi. * Use Databricks query editor components for databricks_internal type of query runner. * URL Escape password in Alembic configuration. * Compare types in migrations.
This commit is contained in:
@@ -1,20 +1,24 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"exclude": [
|
||||
"@babel/plugin-transform-async-to-generator",
|
||||
"@babel/plugin-transform-arrow-functions"
|
||||
],
|
||||
"useBuiltIns": "usage"
|
||||
}],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"exclude": ["@babel/plugin-transform-async-to-generator", "@babel/plugin-transform-arrow-functions"],
|
||||
"corejs": "2",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
],
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-object-assign",
|
||||
["babel-plugin-transform-builtin-extend", {
|
||||
"globals": ["Error"]
|
||||
}]
|
||||
[
|
||||
"babel-plugin-transform-builtin-extend",
|
||||
{
|
||||
"globals": ["Error"]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isFunction, startsWith, trimStart, trimEnd } from "lodash";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useContext } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import UniversalRouter from "universal-router";
|
||||
import ErrorBoundary from "@redash/viz/lib/components/ErrorBoundary";
|
||||
@@ -14,6 +14,12 @@ function generateRouteKey() {
|
||||
.substr(2);
|
||||
}
|
||||
|
||||
export const CurrentRouteContext = React.createContext(null);
|
||||
|
||||
export function useCurrentRoute() {
|
||||
return useContext(CurrentRouteContext);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -53,7 +59,7 @@ export default function Router({ routes, onRouteChange }) {
|
||||
errorHandlerRef.current.reset();
|
||||
}
|
||||
|
||||
const pathname = stripBase(location.path);
|
||||
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
|
||||
@@ -109,9 +115,11 @@ export default function Router({ routes, onRouteChange }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary ref={errorHandlerRef} renderError={error => <ErrorMessage error={error} />}>
|
||||
{currentRoute.render(currentRoute)}
|
||||
</ErrorBoundary>
|
||||
<CurrentRouteContext.Provider value={currentRoute}>
|
||||
<ErrorBoundary ref={errorHandlerRef} renderError={error => <ErrorMessage error={error} />}>
|
||||
{currentRoute.render(currentRoute)}
|
||||
</ErrorBoundary>
|
||||
</CurrentRouteContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
30
client/app/components/DialogWrapper.d.ts
vendored
Normal file
30
client/app/components/DialogWrapper.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ModalProps } from "antd/lib/modal/Modal";
|
||||
|
||||
export interface DialogProps<ROk, RCancel> {
|
||||
props: ModalProps;
|
||||
close: (result: ROk) => void;
|
||||
dismiss: (result: RCancel) => void;
|
||||
}
|
||||
|
||||
export type DialogWrapperChildProps<ROk, RCancel> = {
|
||||
dialog: DialogProps<ROk, RCancel>;
|
||||
};
|
||||
|
||||
export type DialogComponentType<ROk = void, P = {}, RCancel = void> = React.ComponentType<
|
||||
DialogWrapperChildProps<ROk, RCancel> & P
|
||||
>;
|
||||
|
||||
export function wrap<ROk = void, P = {}, RCancel = void>(
|
||||
DialogComponent: DialogComponentType<ROk, P, RCancel>
|
||||
): {
|
||||
Component: DialogComponentType<ROk, P, RCancel>;
|
||||
showModal: (
|
||||
props?: P
|
||||
) => {
|
||||
update: (props: P) => void;
|
||||
onClose: (handler: (result: ROk) => Promise<void>) => void;
|
||||
onDismiss: (handler: (result: RCancel) => Promise<void>) => void;
|
||||
close: (result: ROk) => void;
|
||||
dismiss: (result: RCancel) => void;
|
||||
};
|
||||
};
|
||||
@@ -68,7 +68,7 @@ UserPreviewCard.defaultProps = {
|
||||
// DataSourcePreviewCard
|
||||
|
||||
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
|
||||
const imageUrl = `/static/images/db-logos/${dataSource.type}.png`;
|
||||
const imageUrl = `static/images/db-logos/${dataSource.type}.png`;
|
||||
const title = withLink ? <a href={"data_sources/" + dataSource.id}>{dataSource.name}</a> : dataSource.name;
|
||||
return (
|
||||
<PreviewCard {...props} imageUrl={imageUrl} title={title}>
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
export interface EmptyStateProps {
|
||||
type DefaultStepKey = "dataSources" | "queries" | "alerts" | "dashboards" | "users";
|
||||
export type StepKey<K> = DefaultStepKey | K;
|
||||
|
||||
export interface StepItem<K> {
|
||||
key: StepKey<K>;
|
||||
node: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface EmptyStateProps<K = unknown> {
|
||||
header?: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
illustration: string;
|
||||
illustrationPath?: string;
|
||||
helpLink: string;
|
||||
|
||||
onboardingMode?: boolean;
|
||||
showAlertStep?: boolean;
|
||||
showDashboardStep?: boolean;
|
||||
showDataSourceStep?: boolean;
|
||||
showInviteStep?: boolean;
|
||||
|
||||
getStepsItems?: (items: Array<StepItem<K>>) => Array<StepItem<K>>;
|
||||
}
|
||||
|
||||
declare const EmptyState: React.FunctionComponent<EmptyStateProps>;
|
||||
declare class EmptyState<R> extends React.Component<EmptyStateProps<R>> {}
|
||||
|
||||
export default EmptyState;
|
||||
|
||||
export interface StepProps {
|
||||
show: boolean;
|
||||
completed: boolean;
|
||||
url?: string;
|
||||
urlText?: string;
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export declare const Step: React.FunctionComponent<StepProps>;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { currentUser } from "@/services/auth";
|
||||
import organizationStatus from "@/services/organizationStatus";
|
||||
import "./empty-state.less";
|
||||
|
||||
function Step({ show, completed, text, url, urlText, onClick }) {
|
||||
export function Step({ show, completed, text, url, urlText, onClick }) {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,10 +46,13 @@ function EmptyState({
|
||||
onboardingMode,
|
||||
showAlertStep,
|
||||
showDashboardStep,
|
||||
showDataSourceStep,
|
||||
showInviteStep,
|
||||
getStepsItems,
|
||||
illustrationPath,
|
||||
}) {
|
||||
const isAvailable = {
|
||||
dataSource: true,
|
||||
dataSource: showDataSourceStep,
|
||||
query: true,
|
||||
alert: showAlertStep,
|
||||
dashboard: showDashboardStep,
|
||||
@@ -75,6 +78,92 @@ function EmptyState({
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderDataSourcesStep = () => {
|
||||
if (currentUser.isAdmin) {
|
||||
return (
|
||||
<Step
|
||||
key="dataSources"
|
||||
show={isAvailable.dataSource}
|
||||
completed={isCompleted.dataSource}
|
||||
url="data_sources/new"
|
||||
urlText="Connect"
|
||||
text="a Data Source"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Step
|
||||
key="dataSources"
|
||||
show={isAvailable.dataSource}
|
||||
completed={isCompleted.dataSource}
|
||||
text="Ask an account admin to connect a data source"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultStepsItems = [
|
||||
{
|
||||
key: "dataSources",
|
||||
node: renderDataSourcesStep(),
|
||||
},
|
||||
{
|
||||
key: "queries",
|
||||
node: (
|
||||
<Step
|
||||
key="queries"
|
||||
show={isAvailable.query}
|
||||
completed={isCompleted.query}
|
||||
url="queries/new"
|
||||
urlText="Create"
|
||||
text="your first Query"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "alerts",
|
||||
node: (
|
||||
<Step
|
||||
key="alerts"
|
||||
show={isAvailable.alert}
|
||||
completed={isCompleted.alert}
|
||||
url="alerts/new"
|
||||
urlText="Create"
|
||||
text="your first Alert"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "dashboards",
|
||||
node: (
|
||||
<Step
|
||||
key="dashboards"
|
||||
show={isAvailable.dashboard}
|
||||
completed={isCompleted.dashboard}
|
||||
onClick={showCreateDashboardDialog}
|
||||
urlText="Create"
|
||||
text="your first Dashboard"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "users",
|
||||
node: (
|
||||
<Step
|
||||
key="users"
|
||||
show={isAvailable.inviteUsers}
|
||||
completed={isCompleted.inviteUsers}
|
||||
url="users/new"
|
||||
urlText="Invite"
|
||||
text="your team members"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const stepsItems = getStepsItems ? getStepsItems(defaultStepsItems) : defaultStepsItems;
|
||||
const imageSource = illustrationPath ? illustrationPath : "static/images/illustrations/" + illustration + ".svg";
|
||||
|
||||
return (
|
||||
<div className="empty-state bg-white tiled">
|
||||
<div className="empty-state__summary">
|
||||
@@ -83,60 +172,11 @@ function EmptyState({
|
||||
<i className={icon} />
|
||||
</h2>
|
||||
<p>{description}</p>
|
||||
<img
|
||||
src={"/static/images/illustrations/" + illustration + ".svg"}
|
||||
alt={illustration + " Illustration"}
|
||||
width="75%"
|
||||
/>
|
||||
<img src={imageSource} alt={illustration + " Illustration"} width="75%" />
|
||||
</div>
|
||||
<div className="empty-state__steps">
|
||||
<h4>Let's get started</h4>
|
||||
<ol>
|
||||
{currentUser.isAdmin && (
|
||||
<Step
|
||||
show={isAvailable.dataSource}
|
||||
completed={isCompleted.dataSource}
|
||||
url="data_sources/new"
|
||||
urlText="Connect"
|
||||
text="a Data Source"
|
||||
/>
|
||||
)}
|
||||
{!currentUser.isAdmin && (
|
||||
<Step
|
||||
show={isAvailable.dataSource}
|
||||
completed={isCompleted.dataSource}
|
||||
text="Ask an account admin to connect a data source"
|
||||
/>
|
||||
)}
|
||||
<Step
|
||||
show={isAvailable.query}
|
||||
completed={isCompleted.query}
|
||||
url="queries/new"
|
||||
urlText="Create"
|
||||
text="your first Query"
|
||||
/>
|
||||
<Step
|
||||
show={isAvailable.alert}
|
||||
completed={isCompleted.alert}
|
||||
url="alerts/new"
|
||||
urlText="Create"
|
||||
text="your first Alert"
|
||||
/>
|
||||
<Step
|
||||
show={isAvailable.dashboard}
|
||||
completed={isCompleted.dashboard}
|
||||
onClick={showCreateDashboardDialog}
|
||||
urlText="Create"
|
||||
text="your first Dashboard"
|
||||
/>
|
||||
<Step
|
||||
show={isAvailable.inviteUsers}
|
||||
completed={isCompleted.inviteUsers}
|
||||
url="users/new"
|
||||
urlText="Invite"
|
||||
text="your team members"
|
||||
/>
|
||||
</ol>
|
||||
<ol>{stepsItems.map(item => item.node)}</ol>
|
||||
<p>
|
||||
Need more support?{" "}
|
||||
<a href={helpLink} target="_blank" rel="noopener noreferrer">
|
||||
@@ -154,12 +194,16 @@ EmptyState.propTypes = {
|
||||
header: PropTypes.string,
|
||||
description: PropTypes.string.isRequired,
|
||||
illustration: PropTypes.string.isRequired,
|
||||
illustrationPath: PropTypes.string,
|
||||
helpLink: PropTypes.string.isRequired,
|
||||
|
||||
onboardingMode: PropTypes.bool,
|
||||
showAlertStep: PropTypes.bool,
|
||||
showDashboardStep: PropTypes.bool,
|
||||
showDataSourceStep: PropTypes.bool,
|
||||
showInviteStep: PropTypes.bool,
|
||||
|
||||
getStepItems: PropTypes.func,
|
||||
};
|
||||
|
||||
EmptyState.defaultProps = {
|
||||
@@ -169,6 +213,7 @@ EmptyState.defaultProps = {
|
||||
onboardingMode: false,
|
||||
showAlertStep: false,
|
||||
showDashboardStep: false,
|
||||
showDataSourceStep: true,
|
||||
showInviteStep: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,42 @@ import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import hoistNonReactStatics from "hoist-non-react-statics";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
export interface PaginationOptions {
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
}
|
||||
|
||||
export interface Controller<I, P = any> {
|
||||
params: P; // TODO: Find out what params is (except merging with props)
|
||||
|
||||
isLoaded: boolean;
|
||||
isEmpty: boolean;
|
||||
|
||||
// search
|
||||
searchTerm?: string;
|
||||
updateSearch: (searchTerm: string) => void;
|
||||
|
||||
// tags
|
||||
selectedTags: string[];
|
||||
updateSelectedTags: (selectedTags: string[]) => void;
|
||||
|
||||
// sorting
|
||||
orderByField?: string;
|
||||
orderByReverse: boolean;
|
||||
toggleSorting: (orderByField: string) => void;
|
||||
|
||||
// pagination
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
totalItemsCount: number;
|
||||
pageSizeOptions: number[];
|
||||
pageItems: I[];
|
||||
updatePagination: (options: PaginationOptions) => void; // ({ page: number, itemsPerPage: number }) => void
|
||||
|
||||
handleError: (error: any) => void; // TODO: Find out if error is string or object or Exception.
|
||||
}
|
||||
|
||||
export const ControllerType = PropTypes.shape({
|
||||
// values of props declared by wrapped component and some additional props from items list
|
||||
@@ -35,15 +71,40 @@ export const ControllerType = PropTypes.shape({
|
||||
handleError: PropTypes.func.isRequired, // (error) => void
|
||||
});
|
||||
|
||||
export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
|
||||
class ItemsListWrapper extends React.Component {
|
||||
export type GenericItemSourceError = AxiosError | Error;
|
||||
|
||||
export interface ItemsListWrapperProps {
|
||||
onError?: (error: AxiosError | Error) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ItemsListWrapperState<I, P = any> extends Controller<I, P> {
|
||||
totalCount?: number;
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
type ItemsSource = any; // TODO: Type ItemsSource
|
||||
type StateStorage = any; // TODO: Type StateStore
|
||||
|
||||
export interface ItemsListWrappedComponentProps<I, P = any> {
|
||||
controller: Controller<I, P>;
|
||||
}
|
||||
|
||||
export function wrap<I, P = any>(
|
||||
WrappedComponent: React.ComponentType<ItemsListWrappedComponentProps<I>>,
|
||||
createItemsSource: () => ItemsSource,
|
||||
createStateStorage: () => StateStorage
|
||||
) {
|
||||
class ItemsListWrapper extends React.Component<ItemsListWrapperProps, ItemsListWrapperState<I, P>> {
|
||||
private _itemsSource: ItemsSource;
|
||||
|
||||
static propTypes = {
|
||||
onError: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onError: error => {
|
||||
onError: (error: GenericItemSourceError) => {
|
||||
// Allow calling chain to roll up, and then throw the error in global context
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
@@ -52,7 +113,7 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
|
||||
children: null,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: ItemsListWrapperProps) {
|
||||
super(props);
|
||||
|
||||
const stateStorage = createStateStorage();
|
||||
@@ -73,7 +134,9 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
|
||||
this.setState(this.getState({ ...state, isLoaded: true }));
|
||||
};
|
||||
|
||||
itemsSource.onError = error => this.props.onError(error);
|
||||
itemsSource.onError = (error: GenericItemSourceError) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.props.onError!(error);
|
||||
|
||||
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
||||
const { updatePagination, toggleSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
||||
@@ -93,13 +156,22 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
this._itemsSource.onBeforeUpdate = () => {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
this._itemsSource.onAfterUpdate = () => {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
this._itemsSource.onError = () => {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getState({ isLoaded, totalCount, pageItems, params, ...rest }) {
|
||||
getState({
|
||||
isLoaded,
|
||||
totalCount,
|
||||
pageItems,
|
||||
params,
|
||||
...rest
|
||||
}: ItemsListWrapperState<I, P>): ItemsListWrapperState<I, P> {
|
||||
return {
|
||||
...rest,
|
||||
|
||||
@@ -110,8 +182,8 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
|
||||
|
||||
isLoaded,
|
||||
isEmpty: !isLoaded || totalCount === 0,
|
||||
totalItemsCount: totalCount,
|
||||
pageSizeOptions: clientConfig.pageSizeOptions,
|
||||
totalItemsCount: totalCount || 0,
|
||||
pageSizeOptions: (clientConfig as any).pageSizeOptions, // TODO: Type auth.js
|
||||
pageItems: pageItems || [],
|
||||
};
|
||||
}
|
||||
51
client/app/components/items-list/classes/ItemsSource.d.ts
vendored
Normal file
51
client/app/components/items-list/classes/ItemsSource.d.ts
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface ItemsSourceOptions<I = any> extends Partial<ItemsSourceState> {
|
||||
getRequest?: (params: any, context: any) => any; // TODO: Add stricter types
|
||||
doRequest?: () => any; // TODO: Add stricter type
|
||||
processResults?: () => any; // TODO: Add stricter type
|
||||
isPlainList?: boolean;
|
||||
sortByIteratees?: { [fieldName: string]: (a: I) => number };
|
||||
}
|
||||
|
||||
export interface GetResourceContext extends ItemsSourceState {
|
||||
params: {
|
||||
currentPage: number;
|
||||
// TODO: Add more context parameters
|
||||
};
|
||||
}
|
||||
|
||||
export type GetResourceRequest = any; // TODO: Add stricter type
|
||||
|
||||
export interface ItemsPage<INPUT = any> {
|
||||
count: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
results: INPUT[];
|
||||
}
|
||||
|
||||
export interface ResourceItemsSourceOptions<INPUT = any, ITEM = any> extends ItemsSourceOptions {
|
||||
getResource: (context: GetResourceContext) => (request: GetResourceRequest) => Promise<INPUT[]>;
|
||||
getItemProcessor?: () => (input: INPUT) => ITEM;
|
||||
}
|
||||
|
||||
export type ItemsSourceState<ITEM = any> = {
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
orderByField: string;
|
||||
orderByReverse: boolean;
|
||||
searchTerm: string;
|
||||
selectedTags: string[];
|
||||
totalCount: number;
|
||||
pageItems: ITEM[];
|
||||
allItems: ITEM[] | undefined;
|
||||
params: {
|
||||
pageTitle?: string;
|
||||
} & { [key: string]: string | number };
|
||||
};
|
||||
|
||||
declare class ItemsSource {
|
||||
constructor(options: ItemsSourceOptions);
|
||||
}
|
||||
|
||||
declare class ResourceItemsSource<I> {
|
||||
constructor(options: ResourceItemsSourceOptions<I>);
|
||||
}
|
||||
@@ -10,6 +10,8 @@ export class ItemsSource {
|
||||
|
||||
onError = null;
|
||||
|
||||
sortByIteratees = undefined;
|
||||
|
||||
getCallbackContext = () => null;
|
||||
|
||||
_beforeUpdate() {
|
||||
@@ -61,7 +63,14 @@ export class ItemsSource {
|
||||
});
|
||||
}
|
||||
|
||||
constructor({ getRequest, doRequest, processResults, isPlainList = false, ...defaultState }) {
|
||||
constructor({
|
||||
getRequest,
|
||||
doRequest,
|
||||
processResults,
|
||||
isPlainList = false,
|
||||
sortByIteratees = undefined,
|
||||
...defaultState
|
||||
}) {
|
||||
if (!isFunction(getRequest)) {
|
||||
getRequest = identity;
|
||||
}
|
||||
@@ -70,6 +79,8 @@ export class ItemsSource {
|
||||
? new PlainListFetcher({ getRequest, doRequest, processResults })
|
||||
: new PaginatedListFetcher({ getRequest, doRequest, processResults });
|
||||
|
||||
this.sortByIteratees = sortByIteratees;
|
||||
|
||||
this.setState(defaultState);
|
||||
this._pageItems = [];
|
||||
|
||||
@@ -93,7 +104,7 @@ export class ItemsSource {
|
||||
|
||||
setState(state) {
|
||||
this._paginator = new Paginator(state);
|
||||
this._sorter = new Sorter(state);
|
||||
this._sorter = new Sorter(state, this.sortByIteratees);
|
||||
|
||||
this._searchTerm = state.searchTerm || "";
|
||||
this._selectedTags = state.selectedTags || [];
|
||||
|
||||
@@ -42,7 +42,7 @@ Content.defaultProps = defaultProps;
|
||||
|
||||
// Layout
|
||||
|
||||
export default function Layout({ className, children, ...props }) {
|
||||
export default function Layout({ children, className = undefined, ...props }) {
|
||||
return (
|
||||
<div className={classNames("layout-with-sidebar", className)} {...props}>
|
||||
{children}
|
||||
|
||||
@@ -9,6 +9,9 @@ registerEditorComponent(QueryEditorComponents.SCHEMA_BROWSER, SchemaBrowser);
|
||||
registerEditorComponent(QueryEditorComponents.QUERY_EDITOR, QueryEditor);
|
||||
|
||||
// databricks
|
||||
registerEditorComponent(QueryEditorComponents.SCHEMA_BROWSER, DatabricksSchemaBrowser, ["databricks"]);
|
||||
registerEditorComponent(QueryEditorComponents.SCHEMA_BROWSER, DatabricksSchemaBrowser, [
|
||||
"databricks",
|
||||
"databricks_internal",
|
||||
]);
|
||||
|
||||
export { getEditorComponents };
|
||||
|
||||
@@ -17,7 +17,7 @@ function EmptyState({ title, message, refreshButton }) {
|
||||
<div className="query-results-empty-state">
|
||||
<div className="empty-state-content">
|
||||
<div>
|
||||
<img src="/static/images/illustrations/no-query-results.svg" alt="No Query Results Illustration" />
|
||||
<img src="static/images/illustrations/no-query-results.svg" alt="No Query Results Illustration" />
|
||||
</div>
|
||||
<h3>{title}</h3>
|
||||
<div className="m-b-20">{message}</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fetchDataFromJob } from "@/services/query-result";
|
||||
|
||||
export const SCHEMA_NOT_SUPPORTED = 1;
|
||||
export const SCHEMA_LOAD_ERROR = 2;
|
||||
export const IMG_ROOT = "/static/images/db-logos";
|
||||
export const IMG_ROOT = "static/images/db-logos";
|
||||
|
||||
function mapSchemaColumnsToObject(columns) {
|
||||
return map(columns, column => (isObject(column) ? column : { name: column }));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { axios } from "@/services/axios";
|
||||
|
||||
export const IMG_ROOT = "/static/images/destinations";
|
||||
export const IMG_ROOT = "static/images/destinations";
|
||||
|
||||
const Destination = {
|
||||
query: () => axios.get("api/destinations"),
|
||||
|
||||
10
client/app/services/notification.d.ts
vendored
Normal file
10
client/app/services/notification.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NotificationApi, ArgsProps } from "antd/lib/notification";
|
||||
type simpleFunc = (message: string, description?: string | null, args?: ArgsProps | null) => void;
|
||||
declare const notification: NotificationApi & {
|
||||
success: simpleFunc;
|
||||
error: simpleFunc;
|
||||
info: simpleFunc;
|
||||
warning: simpleFunc;
|
||||
warn: simpleFunc;
|
||||
};
|
||||
export default notification;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { isString, isObject, filter, sortBy } from "lodash";
|
||||
import pathToRegexp from "path-to-regexp";
|
||||
|
||||
function getRouteParamsCount(path) {
|
||||
const tokens = pathToRegexp.parse(path);
|
||||
return filter(tokens, isObject).length;
|
||||
}
|
||||
|
||||
class Routes {
|
||||
_items = [];
|
||||
_sorted = false;
|
||||
|
||||
get items() {
|
||||
if (!this._sorted) {
|
||||
this._items = sortBy(this._items, [
|
||||
item => getRouteParamsCount(item.path), // simple definitions first, with more params - last
|
||||
item => -item.path.length, // longer first
|
||||
item => item.path, // if same type and length - sort alphabetically
|
||||
]);
|
||||
this._sorted = true;
|
||||
}
|
||||
return this._items;
|
||||
}
|
||||
|
||||
register(id, route) {
|
||||
id = isString(id) ? id : null;
|
||||
this.unregister(id);
|
||||
if (isObject(route)) {
|
||||
this._items = [...this._items, { ...route, id }];
|
||||
this._sorted = false;
|
||||
}
|
||||
}
|
||||
|
||||
unregister(id) {
|
||||
if (isString(id)) {
|
||||
// removing item does not break their order (if already sorted)
|
||||
this._items = filter(this._items, item => item.id !== id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Routes();
|
||||
63
client/app/services/routes.ts
Normal file
63
client/app/services/routes.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { isString, isObject, filter, sortBy } from "lodash";
|
||||
import React from "react";
|
||||
import { Context, Route as UniversalRouterRoute } from "universal-router";
|
||||
import pathToRegexp from "path-to-regexp";
|
||||
|
||||
export interface CurrentRoute<P> {
|
||||
id: string | null;
|
||||
key?: string;
|
||||
title: string;
|
||||
routeParams: P;
|
||||
}
|
||||
|
||||
export interface RedashRoute<P = {}, C extends Context = Context, R = any> extends UniversalRouterRoute<C, R> {
|
||||
path: string; // we don't use other UniversalRouterRoute options, path should be available and should be a string
|
||||
key?: string; // generated in Router.jsx
|
||||
title: string;
|
||||
render?: (currentRoute: CurrentRoute<P>) => React.ReactNode;
|
||||
getApiKey?: () => string;
|
||||
}
|
||||
|
||||
interface RouteItem extends RedashRoute<any> {
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
function getRouteParamsCount(path: string) {
|
||||
const tokens = pathToRegexp.parse(path);
|
||||
return filter(tokens, isObject).length;
|
||||
}
|
||||
|
||||
class Routes {
|
||||
_items: RouteItem[] = [];
|
||||
_sorted = false;
|
||||
|
||||
get items(): RouteItem[] {
|
||||
if (!this._sorted) {
|
||||
this._items = sortBy(this._items, [
|
||||
item => getRouteParamsCount(item.path), // simple definitions first, with more params - last
|
||||
item => -item.path.length, // longer first
|
||||
item => item.path, // if same type and length - sort alphabetically
|
||||
]);
|
||||
this._sorted = true;
|
||||
}
|
||||
return this._items;
|
||||
}
|
||||
|
||||
public register<P>(id: string, route: RedashRoute<P>) {
|
||||
const idOrNull = isString(id) ? id : null;
|
||||
this.unregister(idOrNull);
|
||||
if (isObject(route)) {
|
||||
this._items = [...this.items, { ...route, id: idOrNull }];
|
||||
this._sorted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public unregister(id: string | null) {
|
||||
if (isString(id)) {
|
||||
// removing item does not break their order (if already sorted)
|
||||
this._items = filter(this.items, item => item.id !== id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Routes();
|
||||
@@ -11,16 +11,17 @@ config = context.config
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
logger = logging.getLogger("alembic.env")
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
db_url_escaped = current_app.config.get("SQLALCHEMY_DATABASE_URI").replace("%", "%%")
|
||||
config.set_main_option("sqlalchemy.url", db_url_escaped)
|
||||
target_metadata = current_app.extensions["migrate"].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
@@ -59,21 +60,25 @@ def run_migrations_online():
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
if getattr(config.cmd_opts, "autogenerate", False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
logger.info("No changes in schema detected.")
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions["migrate"].configure_args
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
@@ -81,6 +86,7 @@ def run_migrations_online():
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
|
||||
@@ -47,7 +47,7 @@ setup_logging()
|
||||
redis_connection = redis.from_url(settings.REDIS_URL)
|
||||
rq_redis_connection = redis.from_url(settings.RQ_REDIS_URL)
|
||||
mail = Mail()
|
||||
migrate = Migrate()
|
||||
migrate = Migrate(compare_type=True)
|
||||
statsd_client = StatsClient(
|
||||
host=settings.STATSD_HOST, port=settings.STATSD_PORT, prefix=settings.STATSD_PREFIX
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user