mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
10 Commits
25.11.0-de
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43ee21ac20 | ||
|
|
262d46f465 | ||
|
|
bc68b1c38b | ||
|
|
4353a82c7a | ||
|
|
761eb0b68b | ||
|
|
9743820efe | ||
|
|
9d49e0457f | ||
|
|
b5781a8ebe | ||
|
|
b6f4159be9 | ||
|
|
d5fbf547cf |
@@ -9,121 +9,85 @@ const DYNAMIC_DATE_OPTIONS = [
|
||||
name: "This week",
|
||||
value: getDynamicDateRangeFromString("d_this_week"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_this_week")
|
||||
.value()[0]
|
||||
.format("MMM D") +
|
||||
getDynamicDateRangeFromString("d_this_week").value()[0].format("MMM D") +
|
||||
" - " +
|
||||
getDynamicDateRangeFromString("d_this_week")
|
||||
.value()[1]
|
||||
.format("MMM D"),
|
||||
getDynamicDateRangeFromString("d_this_week").value()[1].format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "This month",
|
||||
value: getDynamicDateRangeFromString("d_this_month"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_this_month")
|
||||
.value()[0]
|
||||
.format("MMMM"),
|
||||
label: () => getDynamicDateRangeFromString("d_this_month").value()[0].format("MMMM"),
|
||||
},
|
||||
{
|
||||
name: "This year",
|
||||
value: getDynamicDateRangeFromString("d_this_year"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_this_year")
|
||||
.value()[0]
|
||||
.format("YYYY"),
|
||||
label: () => getDynamicDateRangeFromString("d_this_year").value()[0].format("YYYY"),
|
||||
},
|
||||
{
|
||||
name: "Last week",
|
||||
value: getDynamicDateRangeFromString("d_last_week"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_week")
|
||||
.value()[0]
|
||||
.format("MMM D") +
|
||||
getDynamicDateRangeFromString("d_last_week").value()[0].format("MMM D") +
|
||||
" - " +
|
||||
getDynamicDateRangeFromString("d_last_week")
|
||||
.value()[1]
|
||||
.format("MMM D"),
|
||||
getDynamicDateRangeFromString("d_last_week").value()[1].format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "Last month",
|
||||
value: getDynamicDateRangeFromString("d_last_month"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_month")
|
||||
.value()[0]
|
||||
.format("MMMM"),
|
||||
label: () => getDynamicDateRangeFromString("d_last_month").value()[0].format("MMMM"),
|
||||
},
|
||||
{
|
||||
name: "Last year",
|
||||
value: getDynamicDateRangeFromString("d_last_year"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_year")
|
||||
.value()[0]
|
||||
.format("YYYY"),
|
||||
label: () => getDynamicDateRangeFromString("d_last_year").value()[0].format("YYYY"),
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: getDynamicDateRangeFromString("d_last_7_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_7_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
label: () => getDynamicDateRangeFromString("d_last_7_days").value()[0].format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 14 days",
|
||||
value: getDynamicDateRangeFromString("d_last_14_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_14_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
label: () => getDynamicDateRangeFromString("d_last_14_days").value()[0].format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: getDynamicDateRangeFromString("d_last_30_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_30_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
label: () => getDynamicDateRangeFromString("d_last_30_days").value()[0].format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 60 days",
|
||||
value: getDynamicDateRangeFromString("d_last_60_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_60_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
label: () => getDynamicDateRangeFromString("d_last_60_days").value()[0].format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 90 days",
|
||||
value: getDynamicDateRangeFromString("d_last_90_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_90_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
label: () => getDynamicDateRangeFromString("d_last_90_days").value()[0].format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 12 months",
|
||||
value: getDynamicDateRangeFromString("d_last_12_months"),
|
||||
label: null,
|
||||
},
|
||||
{
|
||||
name: "Last 10 years",
|
||||
value: getDynamicDateRangeFromString("d_last_10_years"),
|
||||
label: null,
|
||||
},
|
||||
];
|
||||
|
||||
const DYNAMIC_DATETIME_OPTIONS = [
|
||||
{
|
||||
name: "Today",
|
||||
value: getDynamicDateRangeFromString("d_today"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_today")
|
||||
.value()[0]
|
||||
.format("MMM D"),
|
||||
label: () => getDynamicDateRangeFromString("d_today").value()[0].format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: getDynamicDateRangeFromString("d_yesterday"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_yesterday")
|
||||
.value()[0]
|
||||
.format("MMM D"),
|
||||
label: () => getDynamicDateRangeFromString("d_yesterday").value()[0].format("MMM D"),
|
||||
},
|
||||
...DYNAMIC_DATE_OPTIONS,
|
||||
];
|
||||
|
||||
@@ -31,7 +31,8 @@ function DashboardSettings({ dashboardConfiguration }) {
|
||||
<Checkbox
|
||||
checked={!!dashboard.dashboard_filters_enabled}
|
||||
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
|
||||
data-test="DashboardFiltersCheckbox">
|
||||
data-test="DashboardFiltersCheckbox"
|
||||
>
|
||||
Use Dashboard Level Filters
|
||||
</Checkbox>
|
||||
</div>
|
||||
@@ -90,9 +91,9 @@ function DashboardComponent(props) {
|
||||
|
||||
const [pageContainer, setPageContainer] = useState(null);
|
||||
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
|
||||
const onParametersEdit = parameters => {
|
||||
const onParametersEdit = (parameters) => {
|
||||
const paramOrder = map(parameters, "name");
|
||||
updateDashboard({ options: { globalParamOrder: paramOrder } });
|
||||
updateDashboard({ options: { ...dashboard.options, globalParamOrder: paramOrder } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -175,7 +176,7 @@ function DashboardPage({ dashboardSlug, dashboardId, onError }) {
|
||||
|
||||
useEffect(() => {
|
||||
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
|
||||
.then(dashboardData => {
|
||||
.then((dashboardData) => {
|
||||
recordEvent("view", "dashboard", dashboardData.id);
|
||||
setDashboard(dashboardData);
|
||||
|
||||
@@ -207,7 +208,7 @@ routes.register(
|
||||
"Dashboards.LegacyViewOrEdit",
|
||||
routeWithUserSession({
|
||||
path: "/dashboard/:dashboardSlug",
|
||||
render: pageProps => <DashboardPage {...pageProps} />,
|
||||
render: (pageProps) => <DashboardPage {...pageProps} />,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -215,6 +216,6 @@ routes.register(
|
||||
"Dashboards.ViewOrEdit",
|
||||
routeWithUserSession({
|
||||
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
|
||||
render: pageProps => <DashboardPage {...pageProps} />,
|
||||
render: (pageProps) => <DashboardPage {...pageProps} />,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { DashboardStatusEnum } from "../hooks/useDashboard";
|
||||
import "./DashboardHeader.less";
|
||||
|
||||
function getDashboardTags() {
|
||||
return getTags("api/dashboards/tags").then(tags => map(tags, t => t.name));
|
||||
return getTags("api/dashboards/tags").then((tags) => map(tags, (t) => t.name));
|
||||
}
|
||||
|
||||
function buttonType(value) {
|
||||
@@ -38,7 +38,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
|
||||
<h3>
|
||||
<EditInPlace
|
||||
isEditable={editingLayout}
|
||||
onDone={name => updateDashboard({ name })}
|
||||
onDone={(name) => updateDashboard({ name })}
|
||||
value={dashboard.name}
|
||||
ignoreBlanks
|
||||
/>
|
||||
@@ -53,7 +53,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
|
||||
isArchived={dashboard.is_archived}
|
||||
canEdit={canEditDashboard}
|
||||
getAvailableTags={getDashboardTags}
|
||||
onEdit={tags => updateDashboard({ tags })}
|
||||
onEdit={(tags) => updateDashboard({ tags })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -89,14 +89,15 @@ function RefreshButton({ dashboardConfiguration }) {
|
||||
placement="bottomRight"
|
||||
overlay={
|
||||
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
|
||||
{refreshRateOptions.map(option => (
|
||||
{refreshRateOptions.map((option) => (
|
||||
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
|
||||
{durationHumanize(option)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
|
||||
</Menu>
|
||||
}>
|
||||
}
|
||||
>
|
||||
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
|
||||
<i className="fa fa-angle-down" aria-hidden="true" />
|
||||
<span className="sr-only">Split button!</span>
|
||||
@@ -166,7 +167,8 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
|
||||
<PlainButton onClick={archive}>Archive</PlainButton>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}>
|
||||
}
|
||||
>
|
||||
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
|
||||
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
|
||||
</Button>
|
||||
@@ -216,7 +218,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
|
||||
type={buttonType(fullscreen)}
|
||||
className="icon-button m-l-5"
|
||||
onClick={toggleFullscreen}
|
||||
aria-label="Toggle fullscreen display">
|
||||
aria-label="Toggle fullscreen display"
|
||||
>
|
||||
<i className="zmdi zmdi-fullscreen" aria-hidden="true" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -229,7 +232,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
|
||||
type={buttonType(dashboard.publicAccessEnabled)}
|
||||
onClick={showShareDashboardDialog}
|
||||
data-test="OpenShareForm"
|
||||
aria-label="Share">
|
||||
aria-label="Share"
|
||||
>
|
||||
<i className="zmdi zmdi-share" aria-hidden="true" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -252,7 +256,11 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
|
||||
doneBtnClickedWhileSaving,
|
||||
dashboardStatus,
|
||||
retrySaveDashboardLayout,
|
||||
saveDashboardParameters,
|
||||
} = dashboardConfiguration;
|
||||
const handleDoneEditing = () => {
|
||||
saveDashboardParameters().then(() => setEditingLayout(false));
|
||||
};
|
||||
let status;
|
||||
if (dashboardStatus === DashboardStatusEnum.SAVED) {
|
||||
status = <span className="save-status">Saved</span>;
|
||||
@@ -277,7 +285,7 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
|
||||
Retry
|
||||
</Button>
|
||||
) : (
|
||||
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={() => setEditingLayout(false)}>
|
||||
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={handleDoneEditing}>
|
||||
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -22,12 +22,12 @@ export { DashboardStatusEnum } from "./useEditModeHandler";
|
||||
|
||||
function getAffectedWidgets(widgets, updatedParameters = []) {
|
||||
return !isEmpty(updatedParameters)
|
||||
? widgets.filter(widget =>
|
||||
? widgets.filter((widget) =>
|
||||
Object.values(widget.getParameterMappings())
|
||||
.filter(({ type }) => type === "dashboard-level")
|
||||
.some(({ mapTo }) =>
|
||||
includes(
|
||||
updatedParameters.map(p => p.name),
|
||||
updatedParameters.map((p) => p.name),
|
||||
mapTo
|
||||
)
|
||||
)
|
||||
@@ -50,7 +50,7 @@ function useDashboard(dashboardData) {
|
||||
[dashboard]
|
||||
);
|
||||
const hasOnlySafeQueries = useMemo(
|
||||
() => every(dashboard.widgets, w => (w.getQuery() ? w.getQuery().is_safe : true)),
|
||||
() => every(dashboard.widgets, (w) => (w.getQuery() ? w.getQuery().is_safe : true)),
|
||||
[dashboard]
|
||||
);
|
||||
|
||||
@@ -67,19 +67,19 @@ function useDashboard(dashboardData) {
|
||||
|
||||
const updateDashboard = useCallback(
|
||||
(data, includeVersion = true) => {
|
||||
setDashboard(currentDashboard => extend({}, currentDashboard, data));
|
||||
setDashboard((currentDashboard) => extend({}, currentDashboard, data));
|
||||
data = { ...data, id: dashboard.id };
|
||||
if (includeVersion) {
|
||||
data = { ...data, version: dashboard.version };
|
||||
}
|
||||
return Dashboard.save(data)
|
||||
.then(updatedDashboard => {
|
||||
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
|
||||
.then((updatedDashboard) => {
|
||||
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
|
||||
if (has(data, "name")) {
|
||||
location.setPath(url.parse(updatedDashboard.url).pathname, true);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
const status = get(error, "response.status");
|
||||
if (status === 403) {
|
||||
notification.error("Dashboard update failed", "Permission Denied.");
|
||||
@@ -102,25 +102,25 @@ function useDashboard(dashboardData) {
|
||||
|
||||
const loadWidget = useCallback((widget, forceRefresh = false) => {
|
||||
widget.getParametersDefs(); // Force widget to read parameters values from URL
|
||||
setDashboard(currentDashboard => extend({}, currentDashboard));
|
||||
setDashboard((currentDashboard) => extend({}, currentDashboard));
|
||||
return widget
|
||||
.load(forceRefresh)
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
// QueryResultErrors are expected
|
||||
if (error instanceof QueryResultError) {
|
||||
return;
|
||||
}
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
|
||||
.finally(() => setDashboard((currentDashboard) => extend({}, currentDashboard)));
|
||||
}, []);
|
||||
|
||||
const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]);
|
||||
const refreshWidget = useCallback((widget) => loadWidget(widget, true), [loadWidget]);
|
||||
|
||||
const removeWidget = useCallback(widgetId => {
|
||||
setDashboard(currentDashboard =>
|
||||
const removeWidget = useCallback((widgetId) => {
|
||||
setDashboard((currentDashboard) =>
|
||||
extend({}, currentDashboard, {
|
||||
widgets: currentDashboard.widgets.filter(widget => widget.id !== undefined && widget.id !== widgetId),
|
||||
widgets: currentDashboard.widgets.filter((widget) => widget.id !== undefined && widget.id !== widgetId),
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
@@ -132,11 +132,11 @@ function useDashboard(dashboardData) {
|
||||
(forceRefresh = false, updatedParameters = []) => {
|
||||
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
|
||||
const loadWidgetPromises = compact(
|
||||
affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error))
|
||||
affectedWidgets.map((widget) => loadWidget(widget, forceRefresh).catch((error) => error))
|
||||
);
|
||||
|
||||
return Promise.all(loadWidgetPromises).then(() => {
|
||||
const queryResults = compact(map(dashboardRef.current.widgets, widget => widget.getQueryResult()));
|
||||
const queryResults = compact(map(dashboardRef.current.widgets, (widget) => widget.getQueryResult()));
|
||||
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
|
||||
setFilters(updatedFilters);
|
||||
});
|
||||
@@ -145,7 +145,7 @@ function useDashboard(dashboardData) {
|
||||
);
|
||||
|
||||
const refreshDashboard = useCallback(
|
||||
updatedParameters => {
|
||||
(updatedParameters) => {
|
||||
if (!refreshing) {
|
||||
setRefreshing(true);
|
||||
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
|
||||
@@ -154,15 +154,30 @@ function useDashboard(dashboardData) {
|
||||
[refreshing, loadDashboard]
|
||||
);
|
||||
|
||||
const saveDashboardParameters = useCallback(() => {
|
||||
const currentDashboard = dashboardRef.current;
|
||||
|
||||
return updateDashboard({
|
||||
options: {
|
||||
...currentDashboard.options,
|
||||
parameters: map(globalParameters, (p) => p.toSaveableObject()),
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.error("Failed to persist parameter values:", error);
|
||||
notification.error("Parameter values could not be saved. Your changes may not be persisted.");
|
||||
throw error;
|
||||
});
|
||||
}, [globalParameters, updateDashboard]);
|
||||
|
||||
const archiveDashboard = useCallback(() => {
|
||||
recordEvent("archive", "dashboard", dashboard.id);
|
||||
Dashboard.delete(dashboard).then(updatedDashboard =>
|
||||
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
|
||||
Dashboard.delete(dashboard).then((updatedDashboard) =>
|
||||
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
|
||||
);
|
||||
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const showShareDashboardDialog = useCallback(() => {
|
||||
const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard));
|
||||
const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
|
||||
|
||||
ShareDashboardDialog.showModal({
|
||||
dashboard,
|
||||
@@ -175,8 +190,8 @@ function useDashboard(dashboardData) {
|
||||
const showAddTextboxDialog = useCallback(() => {
|
||||
TextboxDialog.showModal({
|
||||
isNew: true,
|
||||
}).onClose(text =>
|
||||
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)))
|
||||
}).onClose((text) =>
|
||||
dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
|
||||
);
|
||||
}, [dashboard]);
|
||||
|
||||
@@ -188,13 +203,13 @@ function useDashboard(dashboardData) {
|
||||
.addWidget(visualization, {
|
||||
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
|
||||
})
|
||||
.then(widget => {
|
||||
.then((widget) => {
|
||||
const widgetsToSave = [
|
||||
widget,
|
||||
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
|
||||
];
|
||||
return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
|
||||
setDashboard(currentDashboard => extend({}, currentDashboard))
|
||||
return Promise.all(widgetsToSave.map((w) => w.save())).then(() =>
|
||||
setDashboard((currentDashboard) => extend({}, currentDashboard))
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -238,6 +253,7 @@ function useDashboard(dashboardData) {
|
||||
setRefreshRate,
|
||||
disableRefreshRate,
|
||||
...editModeHandler,
|
||||
saveDashboardParameters,
|
||||
gridDisabled,
|
||||
setGridDisabled,
|
||||
fullscreen,
|
||||
|
||||
@@ -10,9 +10,9 @@ export const urlForDashboard = ({ id, slug }) => `dashboards/${id}-${slug}`;
|
||||
|
||||
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
|
||||
const filters = {};
|
||||
_.each(queryResults, queryResult => {
|
||||
_.each(queryResults, (queryResult) => {
|
||||
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
|
||||
_.each(queryFilters, queryFilter => {
|
||||
_.each(queryFilters, (queryFilter) => {
|
||||
const hasQueryStringValue = _.has(urlParams, queryFilter.name);
|
||||
|
||||
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
|
||||
@@ -44,7 +44,7 @@ function prepareWidgetsForDashboard(widgets) {
|
||||
const defaultWidgetSizeY =
|
||||
Math.max(
|
||||
_.chain(widgets)
|
||||
.map(w => w.options.position.sizeY)
|
||||
.map((w) => w.options.position.sizeY)
|
||||
.max()
|
||||
.value(),
|
||||
20
|
||||
@@ -55,11 +55,11 @@ function prepareWidgetsForDashboard(widgets) {
|
||||
// 2. update position of widgets in each row - place it right below
|
||||
// biggest widget from previous row
|
||||
_.chain(widgets)
|
||||
.sortBy(widget => widget.options.position.row)
|
||||
.groupBy(widget => widget.options.position.row)
|
||||
.sortBy((widget) => widget.options.position.row)
|
||||
.groupBy((widget) => widget.options.position.row)
|
||||
.reduce((row, widgetsAtRow) => {
|
||||
let height = 1;
|
||||
_.each(widgetsAtRow, widget => {
|
||||
_.each(widgetsAtRow, (widget) => {
|
||||
height = Math.max(
|
||||
height,
|
||||
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
|
||||
@@ -74,8 +74,8 @@ function prepareWidgetsForDashboard(widgets) {
|
||||
.value();
|
||||
|
||||
// Sort widgets by updated column and row value
|
||||
widgets = _.sortBy(widgets, widget => widget.options.position.col);
|
||||
widgets = _.sortBy(widgets, widget => widget.options.position.row);
|
||||
widgets = _.sortBy(widgets, (widget) => widget.options.position.col);
|
||||
widgets = _.sortBy(widgets, (widget) => widget.options.position.row);
|
||||
|
||||
return widgets;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
||||
|
||||
// Find first free row for each column
|
||||
const bottomLine = _.chain(existingWidgets)
|
||||
.map(w => {
|
||||
.map((w) => {
|
||||
const options = _.extend({}, w.options);
|
||||
const position = _.extend({ row: 0, sizeY: 0 }, options.position);
|
||||
return {
|
||||
@@ -97,21 +97,24 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
||||
height: position.sizeY,
|
||||
};
|
||||
})
|
||||
.reduce((result, item) => {
|
||||
const from = Math.max(item.left, 0);
|
||||
const to = Math.min(item.right, result.length + 1);
|
||||
for (let i = from; i < to; i += 1) {
|
||||
result[i] = Math.max(result[i], item.bottom);
|
||||
}
|
||||
return result;
|
||||
}, _.map(new Array(dashboardGridOptions.columns), _.constant(0)))
|
||||
.reduce(
|
||||
(result, item) => {
|
||||
const from = Math.max(item.left, 0);
|
||||
const to = Math.min(item.right, result.length + 1);
|
||||
for (let i = from; i < to; i += 1) {
|
||||
result[i] = Math.max(result[i], item.bottom);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
_.map(new Array(dashboardGridOptions.columns), _.constant(0))
|
||||
)
|
||||
.value();
|
||||
|
||||
// Go through columns, pick them by count necessary to hold new block,
|
||||
// and calculate bottom-most free row per group.
|
||||
// Choose group with the top-most free row (comparing to other groups)
|
||||
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
|
||||
.map(col => ({
|
||||
.map((col) => ({
|
||||
col,
|
||||
row: _.chain(bottomLine)
|
||||
.slice(col, col + width)
|
||||
@@ -126,14 +129,14 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
||||
export function Dashboard(dashboard) {
|
||||
_.extend(this, dashboard);
|
||||
Object.defineProperty(this, "url", {
|
||||
get: function() {
|
||||
get: function () {
|
||||
return urlForDashboard(this);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function prepareDashboardWidgets(widgets) {
|
||||
return prepareWidgetsForDashboard(_.map(widgets, widget => new Widget(widget)));
|
||||
return prepareWidgetsForDashboard(_.map(widgets, (widget) => new Widget(widget)));
|
||||
}
|
||||
|
||||
function transformSingle(dashboard) {
|
||||
@@ -154,7 +157,7 @@ function transformResponse(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const saveOrCreateUrl = data => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
|
||||
const saveOrCreateUrl = (data) => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
|
||||
const DashboardService = {
|
||||
get: ({ id, slug }) => {
|
||||
const params = {};
|
||||
@@ -164,12 +167,12 @@ const DashboardService = {
|
||||
return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse);
|
||||
},
|
||||
getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse),
|
||||
save: data => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
|
||||
save: (data) => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
|
||||
delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse),
|
||||
query: params => axios.get("api/dashboards", { params }).then(transformResponse),
|
||||
recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse),
|
||||
myDashboards: params => axios.get("api/dashboards/my", { params }).then(transformResponse),
|
||||
favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
|
||||
query: (params) => axios.get("api/dashboards", { params }).then(transformResponse),
|
||||
recent: (params) => axios.get("api/dashboards/recent", { params }).then(transformResponse),
|
||||
myDashboards: (params) => axios.get("api/dashboards/my", { params }).then(transformResponse),
|
||||
favorites: (params) => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
|
||||
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
|
||||
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
|
||||
fork: ({ id }) => axios.post(`api/dashboards/${id}/fork`, { id }).then(transformResponse),
|
||||
@@ -187,13 +190,13 @@ Dashboard.prototype.canEdit = function canEdit() {
|
||||
Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
||||
const globalParams = {};
|
||||
const queryParams = location.search;
|
||||
_.each(this.widgets, widget => {
|
||||
_.each(this.widgets, (widget) => {
|
||||
if (widget.getQuery()) {
|
||||
const mappings = widget.getParameterMappings();
|
||||
widget
|
||||
.getQuery()
|
||||
.getParametersDefs(false)
|
||||
.forEach(param => {
|
||||
.forEach((param) => {
|
||||
const mapping = mappings[param.name];
|
||||
if (mapping.type === Widget.MappingType.DashboardLevel) {
|
||||
// create global param
|
||||
@@ -210,15 +213,19 @@ Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
||||
});
|
||||
}
|
||||
});
|
||||
const mergedValues = {
|
||||
..._.mapValues(globalParams, (p) => p.value),
|
||||
...Object.fromEntries((this.options.parameters || []).map((param) => [param.name, param.value])),
|
||||
};
|
||||
const resultingGlobalParams = _.values(
|
||||
_.each(globalParams, param => {
|
||||
param.setValue(param.value); // apply global param value to all locals
|
||||
param.fromUrlParams(queryParams); // try to initialize from url (may do nothing)
|
||||
_.each(globalParams, (param) => {
|
||||
param.setValue(mergedValues[param.name]); // apply merged value
|
||||
param.fromUrlParams(queryParams); // allow param-specific parsing logic
|
||||
})
|
||||
);
|
||||
|
||||
// order dashboard params using paramOrder
|
||||
return _.sortBy(resultingGlobalParams, param =>
|
||||
return _.sortBy(resultingGlobalParams, (param) =>
|
||||
_.includes(this.options.globalParamOrder, param.name)
|
||||
? _.indexOf(this.options.globalParamOrder, param.name)
|
||||
: _.size(this.options.globalParamOrder)
|
||||
|
||||
@@ -17,7 +17,9 @@ const DYNAMIC_PREFIX = "d_";
|
||||
* @param now {function(): moment.Moment=} moment - defaults to now
|
||||
* @returns {function(withNow: boolean): [moment.Moment, moment.Moment|undefined]}
|
||||
*/
|
||||
const untilNow = (from, now = () => moment()) => (withNow = true) => [from(), withNow ? now() : undefined];
|
||||
const untilNow =
|
||||
(from, now = () => moment()) =>
|
||||
(withNow = true) => [from(), withNow ? now() : undefined];
|
||||
|
||||
const DYNAMIC_DATE_RANGES = {
|
||||
today: {
|
||||
@@ -26,14 +28,7 @@ const DYNAMIC_DATE_RANGES = {
|
||||
},
|
||||
yesterday: {
|
||||
name: "Yesterday",
|
||||
value: () => [
|
||||
moment()
|
||||
.subtract(1, "day")
|
||||
.startOf("day"),
|
||||
moment()
|
||||
.subtract(1, "day")
|
||||
.endOf("day"),
|
||||
],
|
||||
value: () => [moment().subtract(1, "day").startOf("day"), moment().subtract(1, "day").endOf("day")],
|
||||
},
|
||||
this_week: {
|
||||
name: "This week",
|
||||
@@ -49,36 +44,15 @@ const DYNAMIC_DATE_RANGES = {
|
||||
},
|
||||
last_week: {
|
||||
name: "Last week",
|
||||
value: () => [
|
||||
moment()
|
||||
.subtract(1, "week")
|
||||
.startOf("week"),
|
||||
moment()
|
||||
.subtract(1, "week")
|
||||
.endOf("week"),
|
||||
],
|
||||
value: () => [moment().subtract(1, "week").startOf("week"), moment().subtract(1, "week").endOf("week")],
|
||||
},
|
||||
last_month: {
|
||||
name: "Last month",
|
||||
value: () => [
|
||||
moment()
|
||||
.subtract(1, "month")
|
||||
.startOf("month"),
|
||||
moment()
|
||||
.subtract(1, "month")
|
||||
.endOf("month"),
|
||||
],
|
||||
value: () => [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")],
|
||||
},
|
||||
last_year: {
|
||||
name: "Last year",
|
||||
value: () => [
|
||||
moment()
|
||||
.subtract(1, "year")
|
||||
.startOf("year"),
|
||||
moment()
|
||||
.subtract(1, "year")
|
||||
.endOf("year"),
|
||||
],
|
||||
value: () => [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
|
||||
},
|
||||
last_hour: {
|
||||
name: "Last hour",
|
||||
@@ -94,63 +68,31 @@ const DYNAMIC_DATE_RANGES = {
|
||||
},
|
||||
last_7_days: {
|
||||
name: "Last 7 days",
|
||||
value: untilNow(
|
||||
() =>
|
||||
moment()
|
||||
.subtract(7, "days")
|
||||
.startOf("day"),
|
||||
() => moment().endOf("day")
|
||||
),
|
||||
value: untilNow(() => moment().subtract(7, "days").startOf("day")),
|
||||
},
|
||||
last_14_days: {
|
||||
name: "Last 14 days",
|
||||
value: untilNow(
|
||||
() =>
|
||||
moment()
|
||||
.subtract(14, "days")
|
||||
.startOf("day"),
|
||||
() => moment().endOf("day")
|
||||
),
|
||||
value: untilNow(() => moment().subtract(14, "days").startOf("day")),
|
||||
},
|
||||
last_30_days: {
|
||||
name: "Last 30 days",
|
||||
value: untilNow(
|
||||
() =>
|
||||
moment()
|
||||
.subtract(30, "days")
|
||||
.startOf("day"),
|
||||
() => moment().endOf("day")
|
||||
),
|
||||
value: untilNow(() => moment().subtract(30, "days").startOf("day")),
|
||||
},
|
||||
last_60_days: {
|
||||
name: "Last 60 days",
|
||||
value: untilNow(
|
||||
() =>
|
||||
moment()
|
||||
.subtract(60, "days")
|
||||
.startOf("day"),
|
||||
() => moment().endOf("day")
|
||||
),
|
||||
value: untilNow(() => moment().subtract(60, "days").startOf("day")),
|
||||
},
|
||||
last_90_days: {
|
||||
name: "Last 90 days",
|
||||
value: untilNow(
|
||||
() =>
|
||||
moment()
|
||||
.subtract(90, "days")
|
||||
.startOf("day"),
|
||||
() => moment().endOf("day")
|
||||
),
|
||||
value: untilNow(() => moment().subtract(90, "days").startOf("day")),
|
||||
},
|
||||
last_12_months: {
|
||||
name: "Last 12 months",
|
||||
value: untilNow(
|
||||
() =>
|
||||
moment()
|
||||
.subtract(12, "months")
|
||||
.startOf("day"),
|
||||
() => moment().endOf("day")
|
||||
),
|
||||
value: untilNow(() => moment().subtract(12, "months").startOf("day")),
|
||||
},
|
||||
last_10_years: {
|
||||
name: "Last 10 years",
|
||||
value: untilNow(() => moment().subtract(10, "years").startOf("day")),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -164,7 +106,7 @@ export function isDynamicDateRangeString(value) {
|
||||
}
|
||||
|
||||
export function getDynamicDateRangeStringFromName(dynamicRangeName) {
|
||||
const key = findKey(DYNAMIC_DATE_RANGES, range => range.name === dynamicRangeName);
|
||||
const key = findKey(DYNAMIC_DATE_RANGES, (range) => range.name === dynamicRangeName);
|
||||
return key ? DYNAMIC_PREFIX + key : undefined;
|
||||
}
|
||||
|
||||
@@ -233,7 +175,7 @@ class DateRangeParameter extends Parameter {
|
||||
|
||||
getExecutionValue() {
|
||||
if (this.hasDynamicValue) {
|
||||
const format = date => date.format(DATETIME_FORMATS[this.type]);
|
||||
const format = (date) => date.format(DATETIME_FORMATS[this.type]);
|
||||
const [start, end] = this.normalizedValue.value().map(format);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class Parameter {
|
||||
|
||||
updateLocals() {
|
||||
if (isArray(this.locals)) {
|
||||
each(this.locals, local => {
|
||||
each(this.locals, (local) => {
|
||||
local.setValue(this.value);
|
||||
});
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class Parameter {
|
||||
|
||||
/** Get a saveable version of the Parameter by omitting unnecessary props */
|
||||
toSaveableObject() {
|
||||
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId"]);
|
||||
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId", "locals"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "redash-client",
|
||||
"version": "25.11.0-dev",
|
||||
"version": "25.12.0-dev",
|
||||
"description": "The frontend part of Redash.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
54
poetry.lock
generated
54
poetry.lock
generated
@@ -2078,6 +2078,58 @@ http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "ibm-db"
|
||||
version = "3.2.7"
|
||||
description = "Python DBI driver for DB2 (LUW, zOS, i5)"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "platform_machine == \"x86_64\" or platform_machine == \"AMD64\""
|
||||
files = [
|
||||
{file = "ibm_db-3.2.7-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:7c2b451ffe67be602e93d94b2d2042dd051ec0757cfd6e4d7344cb594f2d3508"},
|
||||
{file = "ibm_db-3.2.7-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:9a1b139a9c21ff7216aac83ba29dceb6c8a9df3d6aee44ff1fe845cb60d3caed"},
|
||||
{file = "ibm_db-3.2.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5e60297b4680cc566caa67f513aa68883ef48b0c612028a38883620807b09c"},
|
||||
{file = "ibm_db-3.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf1c30e67e9e573e33524c393a1426e0dffa2da34ba42a0ec510e0f75766976f"},
|
||||
{file = "ibm_db-3.2.7-cp310-cp310-win32.whl", hash = "sha256:171014c2caa0419055943ff3badae5118cc3a191360f03b80c8366ef374d5c28"},
|
||||
{file = "ibm_db-3.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:3425c158a65dd43e4b09dc968c18042a656ed6ef2e1db0164f032e97681823b7"},
|
||||
{file = "ibm_db-3.2.7-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:ba493d07e1845b8b1169ad27ace92f0ff540cc9a623f2753b8c68dc66c59d7df"},
|
||||
{file = "ibm_db-3.2.7-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:abed0a7c644b9ddf2c49bf5c0938f936f0b2dffd1703c9819440021be141716e"},
|
||||
{file = "ibm_db-3.2.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cabd3d3e8c879ef60d91e1fe1356cf8603f8b4b69cc7dda39d4a8698a055044"},
|
||||
{file = "ibm_db-3.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aab5dceec45d69b0bbd333be66597dbaedf663c6c56a0fbd6196ecd1836e4095"},
|
||||
{file = "ibm_db-3.2.7-cp311-cp311-win32.whl", hash = "sha256:16272ad07912051d9ab5cbe3a9e2d3d888365d071334f9620d8e0b2ed69ee4f9"},
|
||||
{file = "ibm_db-3.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:4b479e92b6954ab7f65c9d247a65fb0cde6a48899f71a8881b58023c0ace1f49"},
|
||||
{file = "ibm_db-3.2.7-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:24e8a538475997f20569f221247808507b63349df51119fe9b2f8e48a0bf6f9b"},
|
||||
{file = "ibm_db-3.2.7-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:24a53fb8e3c200bf2a55095f1ae4c065f2136f8be87ca1db89a874bd82d88ea5"},
|
||||
{file = "ibm_db-3.2.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91f68be7bd0d2940023da43d0a94f196fe267ca825df7874b8174583c8678ea0"},
|
||||
{file = "ibm_db-3.2.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d39fe5001c078f2b824d1805ca9737060203a00a9dd9a8fe4b6f6b32b271cb5"},
|
||||
{file = "ibm_db-3.2.7-cp312-cp312-win32.whl", hash = "sha256:20388753f52050e07e845b74146dbbe3f892dcfdfb015638e8f57c2fb2e056b8"},
|
||||
{file = "ibm_db-3.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:6e507ddf93b8406b0b88ff6bf07658a3100ce98cb1e735e5ec8e0a56e30ea856"},
|
||||
{file = "ibm_db-3.2.7-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:5fead45b6e1a448d90d7bc4fd8a28783988915a7598418f53191a17f4ddac173"},
|
||||
{file = "ibm_db-3.2.7-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c6462dfd79a23824ce726696531a41a6861555ef27e9f050436bf42ad000734d"},
|
||||
{file = "ibm_db-3.2.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c133f3ed5fae6065a8ccd386fd08d8d07d783343635e5d7c0b7a704419a398dc"},
|
||||
{file = "ibm_db-3.2.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb2d36cfcb1c8b7c25cbdd278ac7be381f7f0c0c8bc330349db166ffd0cf3c5"},
|
||||
{file = "ibm_db-3.2.7-cp313-cp313-win32.whl", hash = "sha256:3dc814d7824b4917f73e35c3c050ed1286ccccc1c3433a7c37984a3069664ac2"},
|
||||
{file = "ibm_db-3.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:ff07632b4514f3af8a64e5c8c38b8aef0833642182a737119e5866a320dd0392"},
|
||||
{file = "ibm_db-3.2.7-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0dd69c71df87776a6fbb0612e559dd4bfdb447f5222d2e2aa81bf2ba4f445491"},
|
||||
{file = "ibm_db-3.2.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de1052e11eee5c62fd66c47ee1e6d19c3c7c3690a06c25e8e5c1fcca508f2f5"},
|
||||
{file = "ibm_db-3.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb41cdd4e7456a44ccb5f943f0b37876d36ee96e1e01d9db8b4d13158e2358af"},
|
||||
{file = "ibm_db-3.2.7-cp37-cp37m-win32.whl", hash = "sha256:37139d0d9c690ca1c951fc2367e2a23bcf2fa4bc57f8f8a744d1abd48caacc4d"},
|
||||
{file = "ibm_db-3.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c3f7c4a53fa0fc1dbd85347459af1c50d76d938023a83aea4339599aed1bbc"},
|
||||
{file = "ibm_db-3.2.7-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:156dfb41b4604c5cdaecc4b308a21b2d03017dcd41a0574bb471f1f842c44577"},
|
||||
{file = "ibm_db-3.2.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d0f51e6664e8440dee88b9f1ce3f6a99012bf7631b44bd0c7caac5bbdf2dc0e"},
|
||||
{file = "ibm_db-3.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6e8e8bb410ed51574faea97e3ede181c210648cc25301b95839dd938572ff64"},
|
||||
{file = "ibm_db-3.2.7-cp38-cp38-win32.whl", hash = "sha256:6be996ee77d60dec0ee5790e83694d34ef749e03b8f8c53d5c7613ca149e6d1f"},
|
||||
{file = "ibm_db-3.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:0fb423623c409e3ca9dbed87773f3916928619361c38fcc635b2a0111cdbe916"},
|
||||
{file = "ibm_db-3.2.7-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:92455cbc68702fa29e857dca8997b900efc4bc29a96fc73a0aa6431c2cfa8fcb"},
|
||||
{file = "ibm_db-3.2.7-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:b4b1fb230d7a9653b0c362010b731e0e606fe50410111fdbd1fb68ea6b62fab0"},
|
||||
{file = "ibm_db-3.2.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf68fc63ab201a651a6c3fc47259c72dd502da841832fe96da2f48e292a698b8"},
|
||||
{file = "ibm_db-3.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb22acdbb9f1d9bf82875a848e771bf2c007fa6e91e0fa9ed43ec7490ac72ba"},
|
||||
{file = "ibm_db-3.2.7-cp39-cp39-win32.whl", hash = "sha256:3d0d0fe235e0c16b1b66d11f636c347ec2665cd4d84930bf6153b294b4000bc3"},
|
||||
{file = "ibm_db-3.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:2f8937f42a9b90847bafdd373867d19293e6f3b5d44f6ff0ff8016a387b64920"},
|
||||
{file = "ibm_db-3.2.7.tar.gz", hash = "sha256:b3c3b4550364a43bf1daa4519b668e6e00e7c3935291f8c444c4ec989417e861"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.1"
|
||||
@@ -6012,4 +6064,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.8,<3.11"
|
||||
content-hash = "562c89639f1cfbe1214b8cf6852c9dfdc5a3e988fd92ca50e387d4ae3e95d164"
|
||||
content-hash = "84271dccdac5067fb1940204ec74f10c4063119efd16107327090dcbb5b1b8c5"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "redash"
|
||||
version = "25.11.0-dev"
|
||||
version = "25.12.0-dev"
|
||||
requires-python = ">=3.8"
|
||||
description = "Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data."
|
||||
authors = [
|
||||
@@ -90,6 +90,7 @@ pyodbc = "5.1.0"
|
||||
debugpy = "^1.8.9"
|
||||
paramiko = "3.4.1"
|
||||
oracledb = "2.5.1"
|
||||
ibm-db = { version = "^3.2.7", markers = "platform_machine == 'x86_64' or platform_machine == 'AMD64'" }
|
||||
|
||||
[tool.poetry.group.all_ds]
|
||||
optional = true
|
||||
|
||||
@@ -14,7 +14,7 @@ from redash.app import create_app # noqa
|
||||
from redash.destinations import import_destinations
|
||||
from redash.query_runner import import_query_runners
|
||||
|
||||
__version__ = "25.11.0-dev"
|
||||
__version__ = "25.12.0-dev"
|
||||
|
||||
|
||||
if os.environ.get("REMOTE_DEBUG"):
|
||||
|
||||
@@ -42,7 +42,7 @@ class Webhook(BaseDestination):
|
||||
auth = HTTPBasicAuth(options.get("username"), options.get("password")) if options.get("username") else None
|
||||
resp = requests.post(
|
||||
options.get("url"),
|
||||
data=json_dumps(data),
|
||||
data=json_dumps(data).encode("utf-8"),
|
||||
auth=auth,
|
||||
headers=headers,
|
||||
timeout=5.0,
|
||||
|
||||
@@ -138,6 +138,15 @@ def _get_ssl_config(configuration):
|
||||
return ssl_config
|
||||
|
||||
|
||||
def _parse_dsn(configuration):
|
||||
standard_params = {"user", "password", "host", "port", "dbname"}
|
||||
params = psycopg2.extensions.parse_dsn(configuration.get("dsn", ""))
|
||||
overlap = standard_params.intersection(params.keys())
|
||||
if overlap:
|
||||
raise ValueError("Extra parameters may not contain {}".format(overlap))
|
||||
return params
|
||||
|
||||
|
||||
class PostgreSQL(BaseSQLQueryRunner):
|
||||
noop_query = "SELECT 1"
|
||||
|
||||
@@ -151,6 +160,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
||||
"host": {"type": "string", "default": "127.0.0.1"},
|
||||
"port": {"type": "number", "default": 5432},
|
||||
"dbname": {"type": "string", "title": "Database Name"},
|
||||
"dsn": {"type": "string", "default": "application_name=redash", "title": "Parameters"},
|
||||
"sslmode": {
|
||||
"type": "string",
|
||||
"title": "SSL Mode",
|
||||
@@ -223,7 +233,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
WHERE c.relkind = 'm'
|
||||
AND has_table_privilege(s.nspname || '.' || c.relname, 'select')
|
||||
AND has_table_privilege(quote_ident(s.nspname) || '.' || quote_ident(c.relname), 'select')
|
||||
AND has_schema_privilege(s.nspname, 'usage')
|
||||
|
||||
UNION
|
||||
@@ -234,7 +244,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
||||
data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
AND has_table_privilege(table_schema || '.' || table_name, 'select')
|
||||
AND has_table_privilege(quote_ident(table_schema) || '.' || quote_ident(table_name), 'select')
|
||||
AND has_schema_privilege(table_schema, 'usage')
|
||||
"""
|
||||
|
||||
@@ -244,6 +254,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
||||
|
||||
def _get_connection(self):
|
||||
self.ssl_config = _get_ssl_config(self.configuration)
|
||||
self.dsn = _parse_dsn(self.configuration)
|
||||
connection = psycopg2.connect(
|
||||
user=self.configuration.get("user"),
|
||||
password=self.configuration.get("password"),
|
||||
@@ -252,6 +263,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
||||
dbname=self.configuration.get("dbname"),
|
||||
async_=True,
|
||||
**self.ssl_config,
|
||||
**self.dsn,
|
||||
)
|
||||
|
||||
return connection
|
||||
|
||||
@@ -9,6 +9,7 @@ from redash.models.parameterized_query import (
|
||||
QueryDetachedFromDataSourceError,
|
||||
)
|
||||
from redash.monitor import rq_job_ids
|
||||
from redash.query_runner import NotSupported
|
||||
from redash.tasks.failure_report import track_failure
|
||||
from redash.utils import json_dumps, sentry
|
||||
from redash.worker import get_job_logger, job
|
||||
@@ -177,6 +178,8 @@ def refresh_schema(data_source_id):
|
||||
time.time() - start_time,
|
||||
)
|
||||
statsd_client.incr("refresh_schema.timeout")
|
||||
except NotSupported:
|
||||
logger.debug("Datasource %s does not support schema refresh", ds.name)
|
||||
except Exception:
|
||||
logger.warning("Failed refreshing schema for the data source: %s", ds.name, exc_info=1)
|
||||
statsd_client.incr("refresh_schema.error")
|
||||
|
||||
@@ -211,7 +211,7 @@ def collect_parameters_from_request(args):
|
||||
|
||||
def base_url(org):
|
||||
if settings.MULTI_ORG:
|
||||
return "https://{}/{}".format(settings.HOST, org.slug)
|
||||
return "{}/{}".format(settings.HOST, org.slug)
|
||||
|
||||
return settings.HOST
|
||||
|
||||
|
||||
50
tests/destinations/test_webhook.py
Normal file
50
tests/destinations/test_webhook.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
from redash.destinations.webhook import Webhook
|
||||
from redash.models import Alert
|
||||
|
||||
|
||||
def test_webhook_notify_handles_unicode():
|
||||
# Create a mock alert with all the properties needed by serialize_alert
|
||||
alert = mock.Mock()
|
||||
alert.id = 1
|
||||
alert.name = "Test Alert"
|
||||
alert.custom_subject = "Test Subject With Unicode: 晨"
|
||||
alert.custom_body = "Test Body"
|
||||
alert.options = {}
|
||||
alert.state = "ok"
|
||||
alert.last_triggered_at = None
|
||||
alert.updated_at = "2025-12-02T08:00:00Z"
|
||||
alert.created_at = "2025-12-02T08:00:00Z"
|
||||
alert.rearm = None
|
||||
alert.query_id = 10
|
||||
alert.user_id = 20
|
||||
|
||||
query = mock.Mock()
|
||||
user = mock.Mock()
|
||||
app = mock.Mock()
|
||||
host = "http://redash.local"
|
||||
options = {"url": "https://example.com/webhook", "username": "user", "password": "password"}
|
||||
metadata = {}
|
||||
new_state = Alert.TRIGGERED_STATE
|
||||
destination = Webhook(options)
|
||||
|
||||
with mock.patch("redash.destinations.webhook.requests.post") as mock_post:
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
destination.notify(alert, query, user, new_state, app, host, metadata, options)
|
||||
|
||||
# Get the data passed to the mock
|
||||
call_args, call_kwargs = mock_post.call_args
|
||||
sent_data = call_kwargs["data"]
|
||||
|
||||
# 1. Make sure we send bytes
|
||||
assert isinstance(sent_data, bytes)
|
||||
|
||||
# 2. Make sure the bytes are the correct UTF-8 encoded JSON
|
||||
decoded_data = json.loads(sent_data.decode("utf-8"))
|
||||
assert decoded_data["alert"]["title"] == alert.custom_subject
|
||||
assert "Test Subject With Unicode: 晨" in sent_data.decode("utf-8")
|
||||
@@ -1,6 +1,7 @@
|
||||
import textwrap
|
||||
from unittest import TestCase
|
||||
|
||||
from redash import settings
|
||||
from redash.models import OPERATORS, Alert, db, next_state
|
||||
from tests import BaseTestCase
|
||||
|
||||
@@ -176,16 +177,18 @@ class TestAlertRenderTemplate(BaseTestCase):
|
||||
ALERT_CONDITION equals
|
||||
ALERT_THRESHOLD 5
|
||||
ALERT_NAME %s
|
||||
ALERT_URL https:///default/alerts/%d
|
||||
ALERT_URL %s/default/alerts/%d
|
||||
QUERY_NAME Query
|
||||
QUERY_URL https:///default/queries/%d
|
||||
QUERY_URL %s/default/queries/%d
|
||||
QUERY_RESULT_VALUE 1
|
||||
QUERY_RESULT_ROWS [{'foo': 1}]
|
||||
QUERY_RESULT_COLS [{'name': 'foo', 'type': 'STRING'}]
|
||||
</pre>
|
||||
""" % (
|
||||
alert.name,
|
||||
settings.HOST,
|
||||
alert.id,
|
||||
settings.HOST,
|
||||
alert.query_id,
|
||||
)
|
||||
result = alert.render_template(textwrap.dedent(custom_alert))
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from redash.query_runner.pg import build_schema
|
||||
from redash.query_runner.pg import _parse_dsn, build_schema
|
||||
|
||||
|
||||
class TestParameters(TestCase):
|
||||
def test_parse_dsn(self):
|
||||
configuration = {"dsn": "application_name=redash connect_timeout=5"}
|
||||
self.assertDictEqual(_parse_dsn(configuration), {"application_name": "redash", "connect_timeout": "5"})
|
||||
|
||||
def test_parse_dsn_not_permitted(self):
|
||||
configuration = {"dsn": "password=xyz"}
|
||||
self.assertRaises(ValueError, _parse_dsn, configuration)
|
||||
|
||||
|
||||
class TestBuildSchema(TestCase):
|
||||
|
||||
@@ -336,6 +336,38 @@ export default function GeneralSettings({ options, data, onOptionsChange }: any)
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{includes(["line", "area"], options.globalSeriesType) && (
|
||||
// @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message
|
||||
<Section>
|
||||
<Select
|
||||
label="Line Shape"
|
||||
data-test="Chart.LineShape"
|
||||
defaultValue={options.lineShape}
|
||||
onChange={(val: any) => onOptionsChange({ lineShape: val })}>
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
<Select.Option value="linear" data-test="Chart.LineShape.Linear">
|
||||
Linear
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
</Select.Option>
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
<Select.Option value="spline" data-test="Chart.LineShape.Spline">
|
||||
Spline
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
</Select.Option>
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
<Select.Option value="hv" data-test="Chart.LineShape.HorizontalVertical">
|
||||
Horizontal-Vertical
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
</Select.Option>
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
<Select.Option value="vh" data-test="Chart.LineShape.VerticalHorizontal">
|
||||
Vertical-Horizontal
|
||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{!includes(["custom", "heatmap", "bubble"], options.globalSeriesType) && (
|
||||
// @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message
|
||||
<Section>
|
||||
|
||||
@@ -18,6 +18,7 @@ const DEFAULT_OPTIONS = {
|
||||
coefficient: 1,
|
||||
piesort: true,
|
||||
color_scheme: "Redash",
|
||||
lineShape: "linear",
|
||||
|
||||
// showDataLabels: false, // depends on chart type
|
||||
numberFormat: "0,0[.]00000",
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"x": "x",
|
||||
"y1": "y"
|
||||
},
|
||||
"missingValuesAsZero": true
|
||||
"missingValuesAsZero": true,
|
||||
"lineShape": "linear"
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
@@ -46,6 +47,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"x": "x",
|
||||
"y1": "y"
|
||||
},
|
||||
"missingValuesAsZero": false
|
||||
"missingValuesAsZero": false,
|
||||
"lineShape": "linear"
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
@@ -54,6 +55,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
@@ -68,6 +70,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["", "2 ± 0", "", "4 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"x": "x",
|
||||
"y1": "y"
|
||||
},
|
||||
"missingValuesAsZero": true
|
||||
"missingValuesAsZero": true,
|
||||
"lineShape": "linear"
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
@@ -54,6 +55,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
@@ -68,6 +70,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["0", "2 ± 0", "0", "4 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"x": "x",
|
||||
"y1": "y"
|
||||
},
|
||||
"missingValuesAsZero": true
|
||||
"missingValuesAsZero": true,
|
||||
"lineShape": "linear"
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
@@ -56,6 +57,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
@@ -70,6 +72,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"x": "x",
|
||||
"y1": "y"
|
||||
},
|
||||
"missingValuesAsZero": true
|
||||
"missingValuesAsZero": true,
|
||||
"lineShape": "linear"
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
@@ -56,6 +57,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
@@ -70,6 +72,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"x": "x",
|
||||
"y1": "y"
|
||||
},
|
||||
"missingValuesAsZero": true
|
||||
"missingValuesAsZero": true,
|
||||
"lineShape": "linear"
|
||||
},
|
||||
"data": [
|
||||
{
|
||||
@@ -56,6 +57,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
@@ -70,6 +72,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
|
||||
"line": { "shape": "linear" },
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
|
||||
@@ -39,11 +39,17 @@ function prepareBarSeries(series: any, options: any, additionalOptions: any) {
|
||||
|
||||
function prepareLineSeries(series: any, options: any) {
|
||||
series.mode = "lines" + (options.showDataLabels ? "+text" : "");
|
||||
series.line = {
|
||||
shape: options.lineShape,
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
function prepareAreaSeries(series: any, options: any) {
|
||||
series.mode = "lines" + (options.showDataLabels ? "+text" : "");
|
||||
series.line = {
|
||||
shape: options.lineShape,
|
||||
}
|
||||
series.fill = options.series.stacking ? "tonexty" : "tozeroy";
|
||||
return series;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user