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:
Arik Fraimovich
2020-08-25 14:11:38 +03:00
committed by GitHub
parent 2cc3bd3d54
commit 84d516bfd1
19 changed files with 426 additions and 142 deletions

View File

@@ -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"]
}
]
]
}

View File

@@ -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>
);
}

View 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;
};
};

View File

@@ -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}>

View File

@@ -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>;

View File

@@ -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&apos;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,
};

View File

@@ -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 || [],
};
}

View 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>);
}

View File

@@ -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 || [];

View File

@@ -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}

View File

@@ -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 };

View File

@@ -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>

View File

@@ -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 }));

View File

@@ -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
View 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;

View File

@@ -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();

View 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();

View File

@@ -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:

View File

@@ -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
)