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 ? (