mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Allow users to share aggregated usage information with us (#4108)
* Initial commit of BeaconConsent component * Add comment about being able to change setting * Use <Text> correctly * Final version of consent screen * Show beacon consent message on homepage only if it wasn't enabled already. * Add consent setting to organization settings screen. * Add support for custom message in OrgSetting.save. * Implmenet consent saving. * If consent given, send extra data * Add HelpTrigger * Make CodeClimate happy * Wrap everything with DynamicComponent
This commit is contained in:
83
client/app/components/BeaconConsent.jsx
Normal file
83
client/app/components/BeaconConsent.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Card from 'antd/lib/card';
|
||||
import Button from 'antd/lib/button';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { HelpTrigger } from '@/components/HelpTrigger';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
import OrgSettings from '@/services/organizationSettings';
|
||||
|
||||
const Text = Typography.Text;
|
||||
|
||||
export function BeaconConsent() {
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
if (!clientConfig.showBeaconConsentMessage || hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hideConsentCard = () => {
|
||||
clientConfig.showBeaconConsentMessage = false;
|
||||
setHide(true);
|
||||
};
|
||||
|
||||
const confirmConsent = (confirm) => {
|
||||
let message = '🙏 Thank you.';
|
||||
|
||||
if (!confirm) {
|
||||
message = 'Settings Saved.';
|
||||
}
|
||||
|
||||
OrgSettings.save({ beacon_consent: confirm }, message)
|
||||
// .then(() => {
|
||||
// // const settings = get(response, 'settings');
|
||||
// // this.setState({ settings, formValues: { ...settings } });
|
||||
// })
|
||||
.finally(hideConsentCard);
|
||||
};
|
||||
|
||||
return (
|
||||
<DynamicComponent name="BeaconConsent">
|
||||
<div className="m-t-10 tiled">
|
||||
<Card
|
||||
title={(
|
||||
<>
|
||||
Would you be ok with sharing anonymous usage data with the Redash team?{' '}
|
||||
<HelpTrigger type="USAGE_DATA_SHARING" />
|
||||
</>
|
||||
)}
|
||||
bordered={false}
|
||||
>
|
||||
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
|
||||
<div className="m-t-5">
|
||||
<ul>
|
||||
<li> Number of users, queries, dashboards, alerts, widgets and visualizations.</li>
|
||||
<li> Types of data sources, alert destinations and visualizations.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Text>All data is aggregated and will never include any sensitive or private data.</Text>
|
||||
<div className="m-t-5">
|
||||
<Button type="primary" className="m-r-5" onClick={() => confirmConsent(true)}>
|
||||
Yes
|
||||
</Button>
|
||||
<Button type="default" onClick={() => confirmConsent(false)}>
|
||||
No
|
||||
</Button>
|
||||
</div>
|
||||
<div className="m-t-15">
|
||||
<Text type="secondary">
|
||||
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a> page.
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</DynamicComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('beaconConsent', react2angular(BeaconConsent));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
@@ -32,6 +32,10 @@ export const TYPES = {
|
||||
'/user-guide/users/authentication-options',
|
||||
'Guide: Authentication Options',
|
||||
],
|
||||
USAGE_DATA_SHARING: [
|
||||
'/open-source/admin-guide/usage-data',
|
||||
'Help: Anonymous Usage Data Sharing',
|
||||
],
|
||||
DS_ATHENA: [
|
||||
'/data-sources/amazon-athena-setup',
|
||||
'Guide: Help Setting up Amazon Athena',
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
<div class="container">
|
||||
<div ng-if="$ctrl.messages.includes('using-deprecated-embed-feature')" class="alert alert-warning">
|
||||
You have enabled <code>ALLOW_PARAMETERS_IN_EMBEDS</code>. This setting is now deprecated and should be turned off. Parameters in embeds are supported by default. <a href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337" target="_blank">Read more</a>.
|
||||
<div
|
||||
ng-if="$ctrl.messages.includes('using-deprecated-embed-feature')"
|
||||
class="alert alert-warning"
|
||||
>
|
||||
You have enabled <code>ALLOW_PARAMETERS_IN_EMBEDS</code>. This setting is
|
||||
now deprecated and should be turned off. Parameters in embeds are supported
|
||||
by default.
|
||||
<a
|
||||
href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337"
|
||||
target="_blank"
|
||||
>Read more</a
|
||||
>.
|
||||
</div>
|
||||
<div ng-if="$ctrl.messages.includes('email-not-verified')" class="alert alert-warning">
|
||||
We have sent an email with a confirmation link to your email address. Please follow the link to verify your email address. <a ng-click="$ctrl.verifyEmail()">Resend email</a>.
|
||||
<div
|
||||
ng-if="$ctrl.messages.includes('email-not-verified')"
|
||||
class="alert alert-warning"
|
||||
>
|
||||
We have sent an email with a confirmation link to your email address. Please
|
||||
follow the link to verify your email address.
|
||||
<a ng-click="$ctrl.verifyEmail()">Resend email</a>.
|
||||
</div>
|
||||
<empty-state
|
||||
title="'Welcome to Redash 👋'"
|
||||
@@ -31,13 +46,19 @@
|
||||
</p>
|
||||
|
||||
<div class="list-group">
|
||||
<a ng-href="dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in $ctrl.favoriteDashboards"
|
||||
ng-if="dashboard.is_favorite">
|
||||
<a
|
||||
ng-href="dashboard/{{ dashboard.slug }}"
|
||||
class="list-group-item"
|
||||
ng-repeat="dashboard in $ctrl.favoriteDashboards"
|
||||
ng-if="dashboard.is_favorite"
|
||||
>
|
||||
<span class="btn-favourite">
|
||||
<i class="fa fa-star" aria-hidden="true"></i>
|
||||
</span>
|
||||
{{dashboard.name}}
|
||||
<span class="label label-default" ng-if="dashboard.is_draft">Unpublished</span>
|
||||
{{ dashboard.name }}
|
||||
<span class="label label-default" ng-if="dashboard.is_draft"
|
||||
>Unpublished</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,17 +72,24 @@
|
||||
Favorite <a href="queries">Queries</a> will appear here
|
||||
</p>
|
||||
<div class="list-group">
|
||||
<a ng-href="queries/{{query.id}}" class="list-group-item" ng-repeat="query in $ctrl.favoriteQueries" ng-if="query.is_favorite">
|
||||
<a
|
||||
ng-href="queries/{{ query.id }}"
|
||||
class="list-group-item"
|
||||
ng-repeat="query in $ctrl.favoriteQueries"
|
||||
ng-if="query.is_favorite"
|
||||
>
|
||||
<span class="btn-favourite">
|
||||
<i class="fa fa-star" aria-hidden="true"></i>
|
||||
</span>
|
||||
{{query.name}}
|
||||
<span class="label label-default" ng-if="query.is_draft">Unpublished</span>
|
||||
{{ query.name }}
|
||||
<span class="label label-default" ng-if="query.is_draft"
|
||||
>Unpublished</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<beacon-consent></beacon-consent>
|
||||
</div>
|
||||
|
||||
@@ -10,13 +10,14 @@ import Select from 'antd/lib/select';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import LoadingState from '@/components/items-list/components/LoadingState';
|
||||
import { HelpTrigger } from '@/components/HelpTrigger';
|
||||
|
||||
import { routesToAngularRoutes } from '@/lib/utils';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import settingsMenu from '@/services/settingsMenu';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
import OrgSettings from '@/services/organizationSettings';
|
||||
import { HelpTrigger } from '@/components/HelpTrigger';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
@@ -155,23 +156,6 @@ class OrganizationSettings extends React.Component {
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="Multi-byte Search">
|
||||
<Checkbox
|
||||
name="multi_byte_search_enabled"
|
||||
checked={formValues.multi_byte_search_enabled}
|
||||
onChange={e => this.handleChange('multi_byte_search_enabled', e.target.checked)}
|
||||
>
|
||||
Enable multi-byte (Chinese, Japanese, and Korean) search for query names and descriptions (slower)
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item label="Email Reports">
|
||||
<Checkbox
|
||||
name="send_email_on_failed_scheduled_queries"
|
||||
checked={formValues.send_email_on_failed_scheduled_queries}
|
||||
onChange={e => this.handleChange('send_email_on_failed_scheduled_queries', e.target.checked)}
|
||||
>Email query owners when scheduled queries fail
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item label="Feature Flags">
|
||||
<Checkbox
|
||||
name="feature_show_permissions_control"
|
||||
@@ -181,6 +165,34 @@ class OrganizationSettings extends React.Component {
|
||||
Enable experimental multiple owners support
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox
|
||||
name="send_email_on_failed_scheduled_queries"
|
||||
checked={formValues.send_email_on_failed_scheduled_queries}
|
||||
onChange={e => this.handleChange('send_email_on_failed_scheduled_queries', e.target.checked)}
|
||||
>Email query owners when scheduled queries fail
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Checkbox
|
||||
name="multi_byte_search_enabled"
|
||||
checked={formValues.multi_byte_search_enabled}
|
||||
onChange={e => this.handleChange('multi_byte_search_enabled', e.target.checked)}
|
||||
>
|
||||
Enable multi-byte (Chinese, Japanese, and Korean) search for query names and descriptions (slower)
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<DynamicComponent name="BeaconConsentSetting">
|
||||
<Form.Item label={<>Anonymous Usage Data Sharing <HelpTrigger type="USAGE_DATA_SHARING" /></>}>
|
||||
<Checkbox
|
||||
name="beacon_consent"
|
||||
checked={formValues.beacon_consent}
|
||||
onChange={e => this.handleChange('beacon_consent', e.target.checked)}
|
||||
>
|
||||
Help Redash improve by automatically sending anonymous usage data
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</DynamicComponent>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ import notification from '@/services/notification';
|
||||
|
||||
export default {
|
||||
get: () => $http.get('api/settings/organization').then(response => response.data),
|
||||
save: data => $http.post('api/settings/organization', data).then((response) => {
|
||||
notification.success('Settings changes saved.');
|
||||
return response.data;
|
||||
}).catch(() => {
|
||||
notification.error('Failed saving changes.');
|
||||
}),
|
||||
save: (data, message = 'Settings changes saved.') => $http
|
||||
.post('api/settings/organization', data)
|
||||
.then((response) => {
|
||||
notification.success(message);
|
||||
return response.data;
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error('Failed saving changes.');
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -225,6 +225,9 @@ def client_config():
|
||||
}
|
||||
else:
|
||||
client_config = {}
|
||||
|
||||
if current_user.has_permission('admin') and current_org.get_setting('beacon_consent') is None:
|
||||
client_config['showBeaconConsentMessage'] = True
|
||||
|
||||
defaults = {
|
||||
'allowScriptsInUserInput': settings.ALLOW_SCRIPTS_IN_USER_INPUT,
|
||||
|
||||
@@ -35,6 +35,7 @@ SEND_EMAIL_ON_FAILED_SCHEDULED_QUERIES = parse_boolean(
|
||||
os.environ.get('REDASH_SEND_EMAIL_ON_FAILED_SCHEDULED_QUERIES', 'false'))
|
||||
|
||||
settings = {
|
||||
"beacon_consent": None,
|
||||
"auth_password_login_enabled": PASSWORD_LOGIN_ENABLED,
|
||||
"auth_saml_enabled": SAML_LOGIN_ENABLED,
|
||||
"auth_saml_entity_id": SAML_ENTITY_ID,
|
||||
|
||||
@@ -4,23 +4,74 @@ import semver
|
||||
|
||||
from redash import __version__ as current_version
|
||||
from redash import redis_connection
|
||||
from redash.models import db, Organization
|
||||
from redash.utils import json_dumps
|
||||
|
||||
REDIS_KEY = "new_version_available"
|
||||
|
||||
|
||||
def usage_data():
|
||||
counts_query = """
|
||||
SELECT 'users_count' as name, count(0) as value
|
||||
FROM users
|
||||
WHERE disabled_at is null
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'queries_count' as name, count(0) as value
|
||||
FROM queries
|
||||
WHERE is_archived is false
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'alerts_count' as name, count(0) as value
|
||||
FROM alerts
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'dashboards_count' as name, count(0) as value
|
||||
FROM dashboards
|
||||
WHERE is_archived is false
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'widgets_count' as name, count(0) as value
|
||||
FROM widgets
|
||||
WHERE visualization_id is not null
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'textbox_count' as name, count(0) as value
|
||||
FROM widgets
|
||||
WHERE visualization_id is null
|
||||
"""
|
||||
|
||||
data_sources_query = "SELECT type, count(0) FROM data_sources GROUP by 1"
|
||||
visualizations_query = "SELECT type, count(0) FROM visualizations GROUP by 1"
|
||||
destinations_query = "SELECT type, count(0) FROM notification_destinations GROUP by 1"
|
||||
|
||||
data = {name: value for (name, value) in db.session.execute(counts_query)}
|
||||
data['data_sources'] = {name: value for (name, value) in db.session.execute(data_sources_query)}
|
||||
data['visualization_types'] = {name: value for (name, value) in db.session.execute(visualizations_query)}
|
||||
data['destination_types'] = {name: value for (name, value) in db.session.execute(destinations_query)}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def run_version_check():
|
||||
logging.info("Performing version check.")
|
||||
logging.info("Current version: %s", current_version)
|
||||
|
||||
data = json_dumps({
|
||||
data = {
|
||||
'current_version': current_version
|
||||
})
|
||||
headers = {'content-type': 'application/json'}
|
||||
}
|
||||
|
||||
if Organization.query.first().get_setting('beacon_consent'):
|
||||
data['usage'] = usage_data()
|
||||
|
||||
try:
|
||||
response = requests.post('https://version.redash.io/api/report?channel=stable',
|
||||
data=data, headers=headers, timeout=3.0)
|
||||
json=data, timeout=3.0)
|
||||
latest_version = response.json()['release']['version']
|
||||
|
||||
_compare_and_update(latest_version)
|
||||
|
||||
Reference in New Issue
Block a user