diff --git a/.github/workflows/preview-image.yml b/.github/workflows/preview-image.yml
index ee81a6911f..b650cf89a3 100644
--- a/.github/workflows/preview-image.yml
+++ b/.github/workflows/preview-image.yml
@@ -44,10 +44,10 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- - name: Install Dependencies
- run: |
- npm install --global --force yarn@1.22.22
- yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -58,6 +58,11 @@ jobs:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
+ - name: Install Dependencies
+ run: |
+ npm install --global --force yarn@1.22.22
+ yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
+
- name: Set version
id: version
run: |
@@ -66,6 +71,7 @@ jobs:
VERSION_TAG=$(jq -r .version package.json)
echo "VERSION_TAG=$VERSION_TAG" >> "$GITHUB_OUTPUT"
+ # TODO: We can use GitHub Actions's matrix option to reduce the build time.
- name: Build and push preview image to Docker Hub
uses: docker/build-push-action@v4
with:
@@ -76,9 +82,9 @@ jobs:
context: .
build-args: |
test_all_deps=true
- cache-from: type=gha
- cache-to: type=gha,mode=max
- platforms: linux/amd64
+ cache-from: type=gha,scope=multi-platform
+ cache-to: type=gha,mode=max,scope=multi-platform
+ platforms: linux/amd64,linux/arm64
env:
DOCKER_CONTENT_TRUST: true
diff --git a/.gitignore b/.gitignore
index b324689c96..3fba4897ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ client/dist
_build
.vscode
.env
+.tool-versions
dump.rdb
diff --git a/Dockerfile b/Dockerfile
index 48d91528b0..d7258a9c0d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:18-bookworm as frontend-builder
+FROM node:18-bookworm AS frontend-builder
RUN npm install --global --force yarn@1.22.22
@@ -20,6 +20,9 @@ COPY --chown=redash scripts /frontend/scripts
ARG code_coverage
ENV BABEL_ENV=${code_coverage:+test}
+# Avoid issues caused by lags in disk and network I/O speeds when working on top of QEMU emulation for multi-platform image building.
+RUN yarn config set network-timeout 300000
+
RUN if [ "x$skip_frontend_build" = "x" ] ; then yarn --frozen-lockfile --network-concurrency 1; fi
COPY --chown=redash client /frontend/client
@@ -86,6 +89,9 @@ ENV POETRY_HOME=/etc/poetry
ENV POETRY_VIRTUALENVS_CREATE=false
RUN curl -sSL https://install.python-poetry.org | python3 -
+# Avoid crashes, including corrupted cache artifacts, when building multi-platform images with GitHub Actions.
+RUN /etc/poetry/bin/poetry cache clear pypi --all
+
COPY pyproject.toml poetry.lock ./
ARG POETRY_OPTIONS="--no-root --no-interaction --no-ansi"
diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js
index e491b36166..1a936e3282 100644
--- a/client/app/components/proptypes.js
+++ b/client/app/components/proptypes.js
@@ -65,6 +65,7 @@ export const Query = PropTypes.shape({
export const AlertOptions = PropTypes.shape({
column: PropTypes.string,
+ selector: PropTypes.oneOf(["first", "min", "max"]),
op: PropTypes.oneOf([">", ">=", "<", "<=", "==", "!="]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
custom_subject: PropTypes.string,
@@ -83,6 +84,7 @@ export const Alert = PropTypes.shape({
query: Query,
options: PropTypes.shape({
column: PropTypes.string,
+ selector: PropTypes.string,
op: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}).isRequired,
diff --git a/client/app/pages/alert/Alert.jsx b/client/app/pages/alert/Alert.jsx
index e54ebcb0e6..877646b611 100644
--- a/client/app/pages/alert/Alert.jsx
+++ b/client/app/pages/alert/Alert.jsx
@@ -16,6 +16,7 @@ import MenuButton from "./components/MenuButton";
import AlertView from "./AlertView";
import AlertEdit from "./AlertEdit";
import AlertNew from "./AlertNew";
+import notifications from "@/services/notifications";
const MODES = {
NEW: 0,
@@ -64,6 +65,7 @@ class Alert extends React.Component {
this.setState({
alert: {
options: {
+ selector: "first",
op: ">",
value: 1,
muted: false,
@@ -75,7 +77,7 @@ class Alert extends React.Component {
} else {
const { alertId } = this.props;
AlertService.get({ id: alertId })
- .then(alert => {
+ .then((alert) => {
if (this._isMounted) {
const canEdit = currentUser.canEdit(alert);
@@ -93,7 +95,7 @@ class Alert extends React.Component {
this.onQuerySelected(alert.query);
}
})
- .catch(error => {
+ .catch((error) => {
if (this._isMounted) {
this.props.onError(error);
}
@@ -112,7 +114,7 @@ class Alert extends React.Component {
alert.rearm = pendingRearm || null;
return AlertService.save(alert)
- .then(alert => {
+ .then((alert) => {
notification.success("Saved.");
navigateTo(`alerts/${alert.id}`, true);
this.setState({ alert, mode: MODES.VIEW });
@@ -122,7 +124,7 @@ class Alert extends React.Component {
});
};
- onQuerySelected = query => {
+ onQuerySelected = (query) => {
this.setState(({ alert }) => ({
alert: Object.assign(alert, { query }),
queryResult: null,
@@ -130,7 +132,7 @@ class Alert extends React.Component {
if (query) {
// get cached result for column names and values
- new QueryService(query).getQueryResultPromise().then(queryResult => {
+ new QueryService(query).getQueryResultPromise().then((queryResult) => {
if (this._isMounted) {
this.setState({ queryResult });
let { column } = this.state.alert.options;
@@ -146,18 +148,18 @@ class Alert extends React.Component {
}
};
- onNameChange = name => {
+ onNameChange = (name) => {
const { alert } = this.state;
this.setState({
alert: Object.assign(alert, { name }),
});
};
- onRearmChange = pendingRearm => {
+ onRearmChange = (pendingRearm) => {
this.setState({ pendingRearm });
};
- setAlertOptions = obj => {
+ setAlertOptions = (obj) => {
const { alert } = this.state;
const options = { ...alert.options, ...obj };
this.setState({
@@ -177,6 +179,17 @@ class Alert extends React.Component {
});
};
+ evaluate = () => {
+ const { alert } = this.state;
+ return AlertService.evaluate(alert)
+ .then(() => {
+ notification.success("Alert evaluated. Refresh page for updated status.");
+ })
+ .catch(() => {
+ notifications.error("Failed to evaluate alert.");
+ });
+ };
+
mute = () => {
const { alert } = this.state;
return AlertService.mute(alert)
@@ -223,7 +236,14 @@ class Alert extends React.Component {
const { queryResult, mode, canEdit, pendingRearm } = this.state;
const menuButton = (
-
+
);
const commonProps = {
@@ -258,7 +278,7 @@ routes.register(
routeWithUserSession({
path: "/alerts/new",
title: "New Alert",
- render: pageProps => ,
+ render: (pageProps) => ,
})
);
routes.register(
@@ -266,7 +286,7 @@ routes.register(
routeWithUserSession({
path: "/alerts/:alertId",
title: "Alert",
- render: pageProps => ,
+ render: (pageProps) => ,
})
);
routes.register(
@@ -274,6 +294,6 @@ routes.register(
routeWithUserSession({
path: "/alerts/:alertId/edit",
title: "Alert",
- render: pageProps => ,
+ render: (pageProps) => ,
})
);
diff --git a/client/app/pages/alert/components/Criteria.jsx b/client/app/pages/alert/components/Criteria.jsx
index 672aa86814..644e5d87e6 100644
--- a/client/app/pages/alert/components/Criteria.jsx
+++ b/client/app/pages/alert/components/Criteria.jsx
@@ -54,23 +54,70 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
return null;
})();
- const columnHint = (
-
- Top row value is {toString(columnValue) || "unknown"}
-
- );
+ let columnHint;
+
+ if (alertOptions.selector === "first") {
+ columnHint = (
+
+ Top row value is {toString(columnValue) || "unknown"}
+
+ );
+ } else if (alertOptions.selector === "max") {
+ columnHint = (
+
+ Max column value is{" "}
+
+ {toString(Math.max(...resultValues.map((o) => o[alertOptions.column]))) || "unknown"}
+
+
+ );
+ } else if (alertOptions.selector === "min") {
+ columnHint = (
+
+ Min column value is{" "}
+
+ {toString(Math.min(...resultValues.map((o) => o[alertOptions.column]))) || "unknown"}
+
+
+ );
+ }
return (
+
+ Selector
+ {editMode ? (
+
+ ) : (
+ {alertOptions.selector}
+ )}
+
Value column
{editMode ? (
@@ -83,10 +130,11 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
{editMode ? (