Skip to content

Commit

Permalink
Merge branch 'master' into issue-6640
Browse files Browse the repository at this point in the history
  • Loading branch information
justinclift authored Aug 1, 2024
2 parents 0c03667 + 86b75db commit c4f5b6e
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 48 deletions.
20 changes: 13 additions & 7 deletions .github/workflows/preview-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'

- name: Install Dependencies
run: |
npm install --global --force [email protected]
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
Expand All @@ -58,6 +58,11 @@ jobs:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}

- name: Install Dependencies
run: |
npm install --global --force [email protected]
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
- name: Set version
id: version
run: |
Expand All @@ -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:
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ client/dist
_build
.vscode
.env
.tool-versions

dump.rdb

Expand Down
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18-bookworm as frontend-builder
FROM node:18-bookworm AS frontend-builder

RUN npm install --global --force [email protected]

Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
44 changes: 32 additions & 12 deletions client/app/pages/alert/Alert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -64,6 +65,7 @@ class Alert extends React.Component {
this.setState({
alert: {
options: {
selector: "first",
op: ">",
value: 1,
muted: false,
Expand All @@ -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);

Expand All @@ -93,7 +95,7 @@ class Alert extends React.Component {
this.onQuerySelected(alert.query);
}
})
.catch(error => {
.catch((error) => {
if (this._isMounted) {
this.props.onError(error);
}
Expand All @@ -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 });
Expand All @@ -122,15 +124,15 @@ class Alert extends React.Component {
});
};

onQuerySelected = query => {
onQuerySelected = (query) => {
this.setState(({ alert }) => ({
alert: Object.assign(alert, { query }),
queryResult: null,
}));

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;
Expand All @@ -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({
Expand All @@ -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)
Expand Down Expand Up @@ -223,7 +236,14 @@ class Alert extends React.Component {
const { queryResult, mode, canEdit, pendingRearm } = this.state;

const menuButton = (
<MenuButton doDelete={this.delete} muted={muted} mute={this.mute} unmute={this.unmute} canEdit={canEdit} />
<MenuButton
doDelete={this.delete}
muted={muted}
mute={this.mute}
unmute={this.unmute}
canEdit={canEdit}
evaluate={this.evaluate}
/>
);

const commonProps = {
Expand Down Expand Up @@ -258,22 +278,22 @@ routes.register(
routeWithUserSession({
path: "/alerts/new",
title: "New Alert",
render: pageProps => <Alert {...pageProps} mode={MODES.NEW} />,
render: (pageProps) => <Alert {...pageProps} mode={MODES.NEW} />,
})
);
routes.register(
"Alerts.View",
routeWithUserSession({
path: "/alerts/:alertId",
title: "Alert",
render: pageProps => <Alert {...pageProps} mode={MODES.VIEW} />,
render: (pageProps) => <Alert {...pageProps} mode={MODES.VIEW} />,
})
);
routes.register(
"Alerts.Edit",
routeWithUserSession({
path: "/alerts/:alertId/edit",
title: "Alert",
render: pageProps => <Alert {...pageProps} mode={MODES.EDIT} />,
render: (pageProps) => <Alert {...pageProps} mode={MODES.EDIT} />,
})
);
70 changes: 59 additions & 11 deletions client/app/pages/alert/components/Criteria.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,70 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
return null;
})();

const columnHint = (
<small className="alert-criteria-hint">
Top row value is <code className="p-0">{toString(columnValue) || "unknown"}</code>
</small>
);
let columnHint;

if (alertOptions.selector === "first") {
columnHint = (
<small className="alert-criteria-hint">
Top row value is <code className="p-0">{toString(columnValue) || "unknown"}</code>
</small>
);
} else if (alertOptions.selector === "max") {
columnHint = (
<small className="alert-criteria-hint">
Max column value is{" "}
<code className="p-0">
{toString(Math.max(...resultValues.map((o) => o[alertOptions.column]))) || "unknown"}
</code>
</small>
);
} else if (alertOptions.selector === "min") {
columnHint = (
<small className="alert-criteria-hint">
Min column value is{" "}
<code className="p-0">
{toString(Math.min(...resultValues.map((o) => o[alertOptions.column]))) || "unknown"}
</code>
</small>
);
}

return (
<div data-test="Criteria">
<div className="input-title">
<span className="input-label">Selector</span>
{editMode ? (
<Select
value={alertOptions.selector}
onChange={(selector) => onChange({ selector })}
optionLabelProp="label"
dropdownMatchSelectWidth={false}
style={{ width: 80 }}
>
<Select.Option value="first" label="first">
first
</Select.Option>
<Select.Option value="min" label="min">
min
</Select.Option>
<Select.Option value="max" label="max">
max
</Select.Option>
</Select>
) : (
<DisabledInput minWidth={60}>{alertOptions.selector}</DisabledInput>
)}
</div>
<div className="input-title">
<span className="input-label">Value column</span>
{editMode ? (
<Select
value={alertOptions.column}
onChange={column => onChange({ column })}
onChange={(column) => onChange({ column })}
dropdownMatchSelectWidth={false}
style={{ minWidth: 100 }}>
{columnNames.map(name => (
style={{ minWidth: 100 }}
>
{columnNames.map((name) => (
<Select.Option key={name}>{name}</Select.Option>
))}
</Select>
Expand All @@ -83,10 +130,11 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
{editMode ? (
<Select
value={alertOptions.op}
onChange={op => onChange({ op })}
onChange={(op) => onChange({ op })}
optionLabelProp="label"
dropdownMatchSelectWidth={false}
style={{ width: 55 }}>
style={{ width: 55 }}
>
<Select.Option value=">" label={CONDITIONS[">"]}>
{CONDITIONS[">"]} greater than
</Select.Option>
Expand Down Expand Up @@ -125,7 +173,7 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
id="threshold-criterion"
style={{ width: 90 }}
value={alertOptions.value}
onChange={e => onChange({ value: e.target.value })}
onChange={(e) => onChange({ value: e.target.value })}
/>
) : (
<DisabledInput minWidth={50}>{alertOptions.value}</DisabledInput>
Expand Down
6 changes: 5 additions & 1 deletion client/app/pages/alert/components/MenuButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import LoadingOutlinedIcon from "@ant-design/icons/LoadingOutlined";
import EllipsisOutlinedIcon from "@ant-design/icons/EllipsisOutlined";
import PlainButton from "@/components/PlainButton";

export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
export default function MenuButton({ doDelete, canEdit, mute, unmute, evaluate, muted }) {
const [loading, setLoading] = useState(false);

const execute = useCallback(action => {
Expand Down Expand Up @@ -55,6 +55,9 @@ export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
<Menu.Item>
<PlainButton onClick={confirmDelete}>Delete</PlainButton>
</Menu.Item>
<Menu.Item>
<PlainButton onClick={() => execute(evaluate)}>Evaluate</PlainButton>
</Menu.Item>
</Menu>
}>
<Button aria-label="More actions">
Expand All @@ -69,6 +72,7 @@ MenuButton.propTypes = {
canEdit: PropTypes.bool.isRequired,
mute: PropTypes.func.isRequired,
unmute: PropTypes.func.isRequired,
evaluate: PropTypes.func.isRequired,
muted: PropTypes.bool,
};

Expand Down
1 change: 1 addition & 0 deletions client/app/services/alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const Alert = {
delete: data => axios.delete(`api/alerts/${data.id}`),
mute: data => axios.post(`api/alerts/${data.id}/mute`),
unmute: data => axios.delete(`api/alerts/${data.id}/mute`),
evaluate: data => axios.post(`api/alerts/${data.id}/eval`),
};

export default Alert;
Loading

0 comments on commit c4f5b6e

Please sign in to comment.