Skip to content

Commit 349cd5d

Browse files
arikfrrestyled-commitsgithub-actions[bot]
authored
Bring back version check & beacon reporting (#7211)
Co-authored-by: Restyled.io <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 49277d2 commit 349cd5d

File tree

17 files changed

+324
-23
lines changed

17 files changed

+324
-23
lines changed

.github/workflows/restyled.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v4
1515
with:
16-
ref: ${{ github.event.pull_request.head.sha }}
16+
ref: ${{ github.event.pull_request.head.ref }}
1717

1818
- uses: restyled-io/actions/setup@v4
1919
- id: restyler

client/app/components/ApplicationArea/ApplicationLayout/VersionInfo.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
2-
import { clientConfig } from "@/services/auth";
2+
import Link from "@/components/Link";
3+
import { clientConfig, currentUser } from "@/services/auth";
34
import frontendVersion from "@/version.json";
45

56
export default function VersionInfo() {
@@ -9,6 +10,15 @@ export default function VersionInfo() {
910
Version: {clientConfig.version}
1011
{frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`}
1112
</div>
13+
{clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && (
14+
<div className="m-t-10">
15+
{/* eslint-disable react/jsx-no-target-blank */}
16+
<Link href="https://version.redash.io/" className="update-available" target="_blank" rel="noopener">
17+
Update Available <i className="fa fa-external-link m-l-5" aria-hidden="true" />
18+
<span className="sr-only">(opens in a new tab)</span>
19+
</Link>
20+
</div>
21+
)}
1222
</React.Fragment>
1323
);
1424
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useState } from "react";
2+
import Card from "antd/lib/card";
3+
import Button from "antd/lib/button";
4+
import Typography from "antd/lib/typography";
5+
import { clientConfig } from "@/services/auth";
6+
import Link from "@/components/Link";
7+
import HelpTrigger from "@/components/HelpTrigger";
8+
import DynamicComponent from "@/components/DynamicComponent";
9+
import OrgSettings from "@/services/organizationSettings";
10+
11+
const Text = Typography.Text;
12+
13+
function BeaconConsent() {
14+
const [hide, setHide] = useState(false);
15+
16+
if (!clientConfig.showBeaconConsentMessage || hide) {
17+
return null;
18+
}
19+
20+
const hideConsentCard = () => {
21+
clientConfig.showBeaconConsentMessage = false;
22+
setHide(true);
23+
};
24+
25+
const confirmConsent = (confirm) => {
26+
let message = "🙏 Thank you.";
27+
28+
if (!confirm) {
29+
message = "Settings Saved.";
30+
}
31+
32+
OrgSettings.save({ beacon_consent: confirm }, message)
33+
// .then(() => {
34+
// // const settings = get(response, 'settings');
35+
// // this.setState({ settings, formValues: { ...settings } });
36+
// })
37+
.finally(hideConsentCard);
38+
};
39+
40+
return (
41+
<DynamicComponent name="BeaconConsent">
42+
<div className="m-t-10 tiled">
43+
<Card
44+
title={
45+
<>
46+
Would you be ok with sharing anonymous usage data with the Redash team?{" "}
47+
<HelpTrigger type="USAGE_DATA_SHARING" />
48+
</>
49+
}
50+
bordered={false}
51+
>
52+
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
53+
<div className="m-t-5">
54+
<ul>
55+
<li> Number of users, queries, dashboards, alerts, widgets and visualizations.</li>
56+
<li> Types of data sources, alert destinations and visualizations.</li>
57+
</ul>
58+
</div>
59+
<Text>All data is aggregated and will never include any sensitive or private data.</Text>
60+
<div className="m-t-5">
61+
<Button type="primary" className="m-r-5" onClick={() => confirmConsent(true)}>
62+
Yes
63+
</Button>
64+
<Button type="default" onClick={() => confirmConsent(false)}>
65+
No
66+
</Button>
67+
</div>
68+
<div className="m-t-15">
69+
<Text type="secondary">
70+
You can change this setting anytime from the <Link href="settings/general">Settings</Link> page.
71+
</Text>
72+
</div>
73+
</Card>
74+
</div>
75+
</DynamicComponent>
76+
);
77+
}
78+
79+
export default BeaconConsent;

client/app/components/HelpTrigger.jsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const TYPES = mapValues(
2323
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
2424
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-dashboards", "Guide: Sharing and Embedding Dashboards"],
2525
AUTHENTICATION_OPTIONS: ["/user-guide/users/authentication-options", "Guide: Authentication Options"],
26+
USAGE_DATA_SHARING: ["/open-source/admin-guide/usage-data", "Help: Anonymous Usage Data Sharing"],
2627
DS_ATHENA: ["/data-sources/amazon-athena-setup", "Guide: Help Setting up Amazon Athena"],
2728
DS_BIGQUERY: ["/data-sources/bigquery-setup", "Guide: Help Setting up BigQuery"],
2829
DS_URL: ["/data-sources/querying-urls", "Guide: Help Setting up URL"],
@@ -100,7 +101,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
100101
clearTimeout(this.iframeLoadingTimeout);
101102
}
102103

103-
loadIframe = url => {
104+
loadIframe = (url) => {
104105
clearTimeout(this.iframeLoadingTimeout);
105106
this.setState({ loading: true, error: false });
106107

@@ -115,8 +116,8 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
115116
clearTimeout(this.iframeLoadingTimeout);
116117
};
117118

118-
onPostMessageReceived = event => {
119-
if (!some(allowedDomains, domain => startsWith(event.origin, domain))) {
119+
onPostMessageReceived = (event) => {
120+
if (!some(allowedDomains, (domain) => startsWith(event.origin, domain))) {
120121
return;
121122
}
122123

@@ -133,7 +134,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
133134
return helpTriggerType ? helpTriggerType[0] : this.props.href;
134135
};
135136

136-
openDrawer = e => {
137+
openDrawer = (e) => {
137138
// keep "open in new tab" behavior
138139
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
139140
e.preventDefault();
@@ -143,7 +144,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
143144
}
144145
};
145146

146-
closeDrawer = event => {
147+
closeDrawer = (event) => {
147148
if (event) {
148149
event.preventDefault();
149150
}
@@ -160,7 +161,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
160161
const tooltip = get(types, `${this.props.type}[1]`, this.props.title);
161162
const className = cx("help-trigger", this.props.className);
162163
const url = this.state.currentUrl;
163-
const isAllowedDomain = some(allowedDomains, domain => startsWith(url || targetUrl, domain));
164+
const isAllowedDomain = some(allowedDomains, (domain) => startsWith(url || targetUrl, domain));
164165
const shouldRenderAsLink = this.props.renderAsLink || !isAllowedDomain;
165166

166167
return (
@@ -179,13 +180,15 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
179180
)}
180181
</>
181182
) : null
182-
}>
183+
}
184+
>
183185
<Link
184186
href={url || this.getUrl()}
185187
className={className}
186188
rel="noopener noreferrer"
187189
target="_blank"
188-
onClick={shouldRenderAsLink ? () => {} : this.openDrawer}>
190+
onClick={shouldRenderAsLink ? () => {} : this.openDrawer}
191+
>
189192
{this.props.children}
190193
</Link>
191194
</Tooltip>
@@ -196,7 +199,8 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
196199
visible={this.state.visible}
197200
className={cx("help-drawer", drawerClassName)}
198201
destroyOnClose
199-
width={400}>
202+
width={400}
203+
>
200204
<div className="drawer-wrapper">
201205
<div className="drawer-menu">
202206
{url && (

client/app/pages/home/Home.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Link from "@/components/Link";
66
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
77
import EmptyState, { EmptyStateHelpMessage } from "@/components/empty-state/EmptyState";
88
import DynamicComponent from "@/components/DynamicComponent";
9+
import BeaconConsent from "@/components/BeaconConsent";
910
import PlainButton from "@/components/PlainButton";
1011

1112
import { axios } from "@/services/axios";
@@ -30,7 +31,8 @@ function DeprecatedEmbedFeatureAlert() {
3031
<Link
3132
href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337"
3233
target="_blank"
33-
rel="noopener noreferrer">
34+
rel="noopener noreferrer"
35+
>
3436
Read more
3537
</Link>
3638
.
@@ -42,7 +44,7 @@ function DeprecatedEmbedFeatureAlert() {
4244

4345
function EmailNotVerifiedAlert() {
4446
const verifyEmail = () => {
45-
axios.post("verification_email/").then(data => {
47+
axios.post("verification_email/").then((data) => {
4648
notification.success(data.message);
4749
});
4850
};
@@ -88,6 +90,7 @@ export default function Home() {
8890
</DynamicComponent>
8991
<DynamicComponent name="HomeExtra" />
9092
<DashboardAndQueryFavoritesList />
93+
<BeaconConsent />
9194
</div>
9295
</div>
9396
);
@@ -98,6 +101,6 @@ routes.register(
98101
routeWithUserSession({
99102
path: "/",
100103
title: "Redash",
101-
render: pageProps => <Home {...pageProps} />,
104+
render: (pageProps) => <Home {...pageProps} />,
102105
})
103106
);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from "react";
2+
import Form from "antd/lib/form";
3+
import Checkbox from "antd/lib/checkbox";
4+
import Skeleton from "antd/lib/skeleton";
5+
import HelpTrigger from "@/components/HelpTrigger";
6+
import DynamicComponent from "@/components/DynamicComponent";
7+
import { SettingsEditorPropTypes, SettingsEditorDefaultProps } from "../prop-types";
8+
9+
export default function BeaconConsentSettings(props) {
10+
const { values, onChange, loading } = props;
11+
12+
return (
13+
<DynamicComponent name="OrganizationSettings.BeaconConsentSettings" {...props}>
14+
<Form.Item
15+
label={
16+
<span>
17+
Anonymous Usage Data Sharing
18+
<HelpTrigger className="m-l-5 m-r-5" type="USAGE_DATA_SHARING" />
19+
</span>
20+
}
21+
>
22+
{loading ? (
23+
<Skeleton title={{ width: 300 }} paragraph={false} active />
24+
) : (
25+
<Checkbox
26+
name="beacon_consent"
27+
checked={values.beacon_consent}
28+
onChange={(e) => onChange({ beacon_consent: e.target.checked })}
29+
>
30+
Help Redash improve by automatically sending anonymous usage data
31+
</Checkbox>
32+
)}
33+
</Form.Item>
34+
</DynamicComponent>
35+
);
36+
}
37+
38+
BeaconConsentSettings.propTypes = SettingsEditorPropTypes;
39+
40+
BeaconConsentSettings.defaultProps = SettingsEditorDefaultProps;

client/app/pages/settings/components/GeneralSettings/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import DynamicComponent from "@/components/DynamicComponent";
44
import FormatSettings from "./FormatSettings";
55
import PlotlySettings from "./PlotlySettings";
66
import FeatureFlagsSettings from "./FeatureFlagsSettings";
7+
import BeaconConsentSettings from "./BeaconConsentSettings";
78

89
export default function GeneralSettings(props) {
910
return (
@@ -13,6 +14,7 @@ export default function GeneralSettings(props) {
1314
<FormatSettings {...props} />
1415
<PlotlySettings {...props} />
1516
<FeatureFlagsSettings {...props} />
17+
<BeaconConsentSettings {...props} />
1618
</DynamicComponent>
1719
);
1820
}

redash/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ def create_app():
3636
from .metrics import request as request_metrics
3737
from .models import db, users
3838
from .utils import sentry
39+
from .version_check import reset_new_version_status
3940

4041
sentry.init()
4142
app = Redash()
4243

44+
# Check and update the cached version for use by the client
45+
reset_new_version_status()
46+
4347
security.init_app(app)
4448
request_metrics.init_app(app)
4549
db.init_app(app)

redash/handlers/authentication.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from redash.handlers import routes
1717
from redash.handlers.base import json_response, org_scoped_rule
18+
from redash.version_check import get_latest_version
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -256,11 +257,15 @@ def number_format_config():
256257

257258
def client_config():
258259
if not current_user.is_api_user() and current_user.is_authenticated:
259-
client_config_inner = {
260+
client_config = {
261+
"newVersionAvailable": bool(get_latest_version()),
260262
"version": __version__,
261263
}
262264
else:
263-
client_config_inner = {}
265+
client_config = {}
266+
267+
if current_user.has_permission("admin") and current_org.get_setting("beacon_consent") is None:
268+
client_config["showBeaconConsentMessage"] = True
264269

265270
defaults = {
266271
"allowScriptsInUserInput": settings.ALLOW_SCRIPTS_IN_USER_INPUT,
@@ -280,12 +285,12 @@ def client_config():
280285
"tableCellMaxJSONSize": settings.TABLE_CELL_MAX_JSON_SIZE,
281286
}
282287

283-
client_config_inner.update(defaults)
284-
client_config_inner.update({"basePath": base_href()})
285-
client_config_inner.update(date_time_format_config())
286-
client_config_inner.update(number_format_config())
288+
client_config.update(defaults)
289+
client_config.update({"basePath": base_href()})
290+
client_config.update(date_time_format_config())
291+
client_config.update(number_format_config())
287292

288-
return client_config_inner
293+
return client_config
289294

290295

291296
def messages():

redash/handlers/setup.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
from flask import g, redirect, render_template, request, url_for
22
from flask_login import login_user
3-
from wtforms import Form, PasswordField, StringField, validators
3+
from wtforms import BooleanField, Form, PasswordField, StringField, validators
44
from wtforms.fields.html5 import EmailField
55

66
from redash import settings
77
from redash.authentication.org_resolving import current_org
88
from redash.handlers.base import routes
99
from redash.models import Group, Organization, User, db
10+
from redash.tasks.general import subscribe
1011

1112

1213
class SetupForm(Form):
1314
name = StringField("Name", validators=[validators.InputRequired()])
1415
email = EmailField("Email Address", validators=[validators.Email()])
1516
password = PasswordField("Password", validators=[validators.Length(6)])
1617
org_name = StringField("Organization Name", validators=[validators.InputRequired()])
18+
security_notifications = BooleanField()
19+
newsletter = BooleanField()
1720

1821

1922
def create_org(org_name, user_name, email, password):
@@ -54,13 +57,19 @@ def setup():
5457
return redirect("/")
5558

5659
form = SetupForm(request.form)
60+
form.newsletter.data = True
61+
form.security_notifications.data = True
5762

5863
if request.method == "POST" and form.validate():
5964
default_org, user = create_org(form.org_name.data, form.name.data, form.email.data, form.password.data)
6065

6166
g.org = default_org
6267
login_user(user)
6368

69+
# signup to newsletter if needed
70+
if form.newsletter.data or form.security_notifications:
71+
subscribe.delay(form.data)
72+
6473
return redirect(url_for("redash.index", org_slug=None))
6574

6675
return render_template("setup.html", form=form)

0 commit comments

Comments
 (0)