mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 17:38:54 +00:00
Load currentUser/clientConfig from server
This commit is contained in:
parent
8676eb000d
commit
4ef4f98a66
8
frontend/app/components/footer/footer.html
Normal file
8
frontend/app/components/footer/footer.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div id="footer">
|
||||||
|
<a href="http://redash.io">Redash</a> <span ng-bind="$ctrl.version"></span> <small ng-if="$ctrl.newVersionAvailable" ng-cloak class="ng-cloak"><a href="https://version.redash.io/">(New Redash version available)</a></small>
|
||||||
|
|
||||||
|
<ul class="f-menu">
|
||||||
|
<li><a href="https://redash.io/help/">Documentation</a></li>
|
||||||
|
<li><a href="http://github.com/getredash/redash">Contribute</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
@ -1,10 +1,13 @@
|
|||||||
function controller() {
|
import template from './footer.html';
|
||||||
|
|
||||||
|
function controller(clientConfig, currentUser) {
|
||||||
|
this.version = clientConfig.version;
|
||||||
|
this.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (ngModule) {
|
export default function (ngModule) {
|
||||||
ngModule.component('footer', {
|
ngModule.component('footer', {
|
||||||
template: '<div>Footer</div>',
|
template,
|
||||||
controller,
|
controller,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,4 @@ export { default as dynamicForm } from './dynamic-form';
|
|||||||
export { default as rdTimer } from './rd-timer';
|
export { default as rdTimer } from './rd-timer';
|
||||||
export { default as rdTimeAgo } from './rd-time-ago';
|
export { default as rdTimeAgo } from './rd-time-ago';
|
||||||
export { default as overlay } from './overlay';
|
export { default as overlay } from './overlay';
|
||||||
|
export { default as routeStatus } from './route-status';
|
||||||
|
19
frontend/app/components/route-status.js
Normal file
19
frontend/app/components/route-status.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.component('routeStatus', {
|
||||||
|
template: '<overlay ng-if="$ctrl.permissionDenied">You do not have permission to load this page.',
|
||||||
|
|
||||||
|
controller($rootScope) {
|
||||||
|
this.permissionDenied = false;
|
||||||
|
|
||||||
|
$rootScope.$on('$routeChangeSuccess', () => {
|
||||||
|
this.permissionDenied = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$rootScope.$on('$routeChangeError', (event, current, previous, rejection) => {
|
||||||
|
if (rejection.status === 403) {
|
||||||
|
this.permissionDenied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -12,7 +12,10 @@
|
|||||||
<body ng-app="app">
|
<body ng-app="app">
|
||||||
<section>
|
<section>
|
||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
|
<route-status></route-status>
|
||||||
<div ng-view></div>
|
<div ng-view></div>
|
||||||
|
<footer>
|
||||||
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -46,47 +46,6 @@ const requirements = [
|
|||||||
|
|
||||||
const ngModule = angular.module('app', requirements);
|
const ngModule = angular.module('app', requirements);
|
||||||
|
|
||||||
// stub for currentUser until we have something real.
|
|
||||||
const user = {
|
|
||||||
name: 'Arik Fraimovich',
|
|
||||||
gravatar_url: 'https://www.gravatar.com/avatar/ca410c2e27337c8d7075bb1b098ac70f?s=40',
|
|
||||||
id: 1,
|
|
||||||
groups: [
|
|
||||||
3,
|
|
||||||
1,
|
|
||||||
],
|
|
||||||
email: 'arik@redash.io',
|
|
||||||
permissions: [
|
|
||||||
'admin',
|
|
||||||
'super_admin',
|
|
||||||
'create_dashboard',
|
|
||||||
'create_query',
|
|
||||||
'edit_dashboard',
|
|
||||||
'edit_query',
|
|
||||||
'view_query',
|
|
||||||
'view_source',
|
|
||||||
'list_users',
|
|
||||||
'execute_query',
|
|
||||||
'schedule_query',
|
|
||||||
'list_dashboards',
|
|
||||||
'list_alerts',
|
|
||||||
'create_alerts',
|
|
||||||
'list_dashboards',
|
|
||||||
'list_alerts',
|
|
||||||
'list_data_sources',
|
|
||||||
],
|
|
||||||
isAdmin: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
user.hasPermission = () => true;
|
|
||||||
user.canEdit = () => true;
|
|
||||||
ngModule.constant('currentUser', user);
|
|
||||||
ngModule.constant('clientConfig', { // TODO: make me a service.
|
|
||||||
showPermissionsControl: true,
|
|
||||||
allowCustomJSVisualizations: true,
|
|
||||||
// mailSettingsMissing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
function registerComponents() {
|
function registerComponents() {
|
||||||
each(components, (register) => {
|
each(components, (register) => {
|
||||||
register(ngModule);
|
register(ngModule);
|
||||||
@ -103,9 +62,14 @@ function registerPages() {
|
|||||||
each(pages, (registerPage) => {
|
each(pages, (registerPage) => {
|
||||||
const routes = registerPage(ngModule);
|
const routes = registerPage(ngModule);
|
||||||
|
|
||||||
|
function session(Auth) {
|
||||||
|
return Auth.loadSession();
|
||||||
|
}
|
||||||
|
|
||||||
ngModule.config(($routeProvider) => {
|
ngModule.config(($routeProvider) => {
|
||||||
each(routes, (route, path) => {
|
each(routes, (route, path) => {
|
||||||
logger('Route: ', path);
|
logger('Route: ', path);
|
||||||
|
route.resolve = Object.assign(route.resolve || {}, { session });
|
||||||
$routeProvider.when(path, route);
|
$routeProvider.when(path, route);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -146,4 +110,10 @@ ngModule.config(($routeProvider,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ngModule.run(($location, Auth) => {
|
||||||
|
if (!Auth.isAuthenticated()) {
|
||||||
|
Auth.login();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default ngModule;
|
export default ngModule;
|
||||||
|
@ -2,7 +2,7 @@ import moment from 'moment';
|
|||||||
import template from './outdated-queries.html';
|
import template from './outdated-queries.html';
|
||||||
|
|
||||||
function OutdatedQueriesCtrl($scope, NgTableParams, currentUser, Events, $http, $timeout) {
|
function OutdatedQueriesCtrl($scope, NgTableParams, currentUser, Events, $http, $timeout) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/outdated_queries');
|
Events.record('view', 'page', 'admin/outdated_queries');
|
||||||
// $scope.$parent.pageTitle = 'Outdated Queries';
|
// $scope.$parent.pageTitle = 'Outdated Queries';
|
||||||
$scope.autoUpdate = true;
|
$scope.autoUpdate = true;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import template from './status.html';
|
|||||||
|
|
||||||
// TODO: switch to $ctrl instead of $scope.
|
// TODO: switch to $ctrl instead of $scope.
|
||||||
function AdminStatusCtrl($scope, $http, $timeout, currentUser, Events) {
|
function AdminStatusCtrl($scope, $http, $timeout, currentUser, Events) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/status');
|
Events.record('view', 'page', 'admin/status');
|
||||||
// $scope.$parent.pageTitle = 'System Status';
|
// $scope.$parent.pageTitle = 'System Status';
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
|
@ -20,7 +20,7 @@ function cancelQueryButton() {
|
|||||||
queryId = null;
|
queryId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.record(currentUser, 'cancel_execute', 'query', queryId, { admin: true });
|
Events.record('cancel_execute', 'query', queryId, { admin: true });
|
||||||
$scope.inProgress = true;
|
$scope.inProgress = true;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,7 @@ import template from './tasks.html';
|
|||||||
import registerCancelQueryButton from './cancel-query-button';
|
import registerCancelQueryButton from './cancel-query-button';
|
||||||
|
|
||||||
function TasksCtrl($scope, $location, $http, $timeout, NgTableParams, currentUser, Events) {
|
function TasksCtrl($scope, $location, $http, $timeout, NgTableParams, currentUser, Events) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/tasks');
|
Events.record('view', 'page', 'admin/tasks');
|
||||||
// $scope.$parent.pageTitle = 'Running Queries';
|
// $scope.$parent.pageTitle = 'Running Queries';
|
||||||
$scope.autoUpdate = true;
|
$scope.autoUpdate = true;
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev
|
|||||||
this.alertId = $routeParams.alertId;
|
this.alertId = $routeParams.alertId;
|
||||||
|
|
||||||
if (this.alertId === 'new') {
|
if (this.alertId === 'new') {
|
||||||
Events.record(currentUser, 'view', 'page', 'alerts/new');
|
Events.record('view', 'page', 'alerts/new');
|
||||||
} else {
|
} else {
|
||||||
Events.record(currentUser, 'view', 'alert', this.alertId);
|
Events.record('view', 'alert', this.alertId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.trustAsHtml = html => $sce.trustAsHtml(html);
|
this.trustAsHtml = html => $sce.trustAsHtml(html);
|
||||||
|
@ -2,7 +2,7 @@ import template from './alerts-list.html';
|
|||||||
|
|
||||||
class AlertsListCtrl {
|
class AlertsListCtrl {
|
||||||
constructor(NgTableParams, currentUser, Events, Alert) {
|
constructor(NgTableParams, currentUser, Events, Alert) {
|
||||||
Events.record(currentUser, 'view', 'page', 'alerts');
|
Events.record('view', 'page', 'alerts');
|
||||||
// $scope.$parent.pageTitle = "Alerts";
|
// $scope.$parent.pageTitle = "Alerts";
|
||||||
|
|
||||||
this.tableParams = new NgTableParams({ count: 50 }, {});
|
this.tableParams = new NgTableParams({ count: 50 }, {});
|
||||||
|
@ -82,7 +82,7 @@ function DashboardCtrl($routeParams, $location, $timeout, $q, $uibModal,
|
|||||||
|
|
||||||
this.loadDashboard = _.throttle((force) => {
|
this.loadDashboard = _.throttle((force) => {
|
||||||
this.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, (dashboard) => {
|
this.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, (dashboard) => {
|
||||||
Events.record(currentUser, 'view', 'dashboard', dashboard.id);
|
Events.record('view', 'dashboard', dashboard.id);
|
||||||
renderDashboard(dashboard, force);
|
renderDashboard(dashboard, force);
|
||||||
}, () => {
|
}, () => {
|
||||||
// error...
|
// error...
|
||||||
@ -104,7 +104,7 @@ function DashboardCtrl($routeParams, $location, $timeout, $q, $uibModal,
|
|||||||
|
|
||||||
this.archiveDashboard = () => {
|
this.archiveDashboard = () => {
|
||||||
const archive = () => {
|
const archive = () => {
|
||||||
Events.record(currentUser, 'archive', 'dashboard', this.dashboard.id);
|
Events.record('archive', 'dashboard', this.dashboard.id);
|
||||||
this.dashboard.$delete(() => {
|
this.dashboard.$delete(() => {
|
||||||
// TODO:
|
// TODO:
|
||||||
// this.$parent.reloadDashboards();
|
// this.$parent.reloadDashboards();
|
||||||
|
@ -79,7 +79,7 @@ const EditDashboardDialog = {
|
|||||||
'Please copy/backup your changes and reload this page.', { autoDismiss: false });
|
'Please copy/backup your changes and reload this page.', { autoDismiss: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Events.record(currentUser, 'edit', 'dashboard', this.dashboard.id);
|
Events.record('edit', 'dashboard', this.dashboard.id);
|
||||||
} else {
|
} else {
|
||||||
$http.post('api/dashboards', {
|
$http.post('api/dashboards', {
|
||||||
name: this.dashboard.name,
|
name: this.dashboard.name,
|
||||||
@ -87,7 +87,7 @@ const EditDashboardDialog = {
|
|||||||
this.close();
|
this.close();
|
||||||
$location.path(`/dashboard/${response.slug}`).replace();
|
$location.path(`/dashboard/${response.slug}`).replace();
|
||||||
});
|
});
|
||||||
Events.record(currentUser, 'create', 'dashboard');
|
Events.record('create', 'dashboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -41,7 +41,7 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.record(currentUser, 'delete', 'widget', this.widget.id);
|
Events.record('delete', 'widget', this.widget.id);
|
||||||
|
|
||||||
this.widget.$delete((response) => {
|
this.widget.$delete((response) => {
|
||||||
this.dashboard.widgets =
|
this.dashboard.widgets =
|
||||||
@ -54,7 +54,7 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Events.record(currentUser, 'view', 'widget', this.widget.id);
|
Events.record('view', 'widget', this.widget.id);
|
||||||
|
|
||||||
this.reload = (force) => {
|
this.reload = (force) => {
|
||||||
let maxAge = $location.search().maxAge;
|
let maxAge = $location.search().maxAge;
|
||||||
@ -65,8 +65,8 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.widget.visualization) {
|
if (this.widget.visualization) {
|
||||||
Events.record(currentUser, 'view', 'query', this.widget.visualization.query.id);
|
Events.record('view', 'query', this.widget.visualization.query.id);
|
||||||
Events.record(currentUser, 'view', 'visualization', this.widget.visualization.id);
|
Events.record('view', 'visualization', this.widget.visualization.id);
|
||||||
|
|
||||||
this.query = this.widget.getQuery();
|
this.query = this.widget.getQuery();
|
||||||
this.reload(false);
|
this.reload(false);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import template from './list.html';
|
import template from './list.html';
|
||||||
|
|
||||||
function DataSourcesCtrl($scope, $location, currentUser, Events, DataSource) {
|
function DataSourcesCtrl($scope, $location, currentUser, Events, DataSource) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/data_sources');
|
Events.record('view', 'page', 'admin/data_sources');
|
||||||
$scope.$parent.pageTitle = 'Data Sources';
|
$scope.$parent.pageTitle = 'Data Sources';
|
||||||
|
|
||||||
$scope.dataSources = DataSource.query();
|
$scope.dataSources = DataSource.query();
|
||||||
|
@ -5,7 +5,7 @@ const logger = debug('redash:http');
|
|||||||
|
|
||||||
function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
|
function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
|
||||||
currentUser, Events, DataSource) {
|
currentUser, Events, DataSource) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/data_source');
|
Events.record('view', 'page', 'admin/data_source');
|
||||||
// $scope.$parent.pageTitle = 'Data Sources';
|
// $scope.$parent.pageTitle = 'Data Sources';
|
||||||
|
|
||||||
$scope.dataSourceId = $routeParams.dataSourceId;
|
$scope.dataSourceId = $routeParams.dataSourceId;
|
||||||
@ -23,7 +23,7 @@ function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
|
|||||||
});
|
});
|
||||||
|
|
||||||
function deleteDataSource() {
|
function deleteDataSource() {
|
||||||
Events.record(currentUser, 'delete', 'datasource', $scope.dataSource.id);
|
Events.record('delete', 'datasource', $scope.dataSource.id);
|
||||||
|
|
||||||
$scope.dataSource.$delete(() => {
|
$scope.dataSource.$delete(() => {
|
||||||
toastr.success('Data source deleted successfully.');
|
toastr.success('Data source deleted successfully.');
|
||||||
@ -35,7 +35,7 @@ function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function testConnection(callback) {
|
function testConnection(callback) {
|
||||||
Events.record(currentUser, 'test', 'datasource', $scope.dataSource.id);
|
Events.record('test', 'datasource', $scope.dataSource.id);
|
||||||
|
|
||||||
DataSource.test({ id: $scope.dataSource.id }, (httpResponse) => {
|
DataSource.test({ id: $scope.dataSource.id }, (httpResponse) => {
|
||||||
if (httpResponse.ok) {
|
if (httpResponse.ok) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import template from './list.html';
|
import template from './list.html';
|
||||||
|
|
||||||
function DestinationsCtrl($scope, $location, toastr, currentUser, Events, Destination) {
|
function DestinationsCtrl($scope, $location, toastr, currentUser, Events, Destination) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/destinations');
|
Events.record('view', 'page', 'admin/destinations');
|
||||||
// $scope.$parent.pageTitle = 'Destinations';
|
// $scope.$parent.pageTitle = 'Destinations';
|
||||||
|
|
||||||
$scope.destinations = Destination.query();
|
$scope.destinations = Destination.query();
|
||||||
|
@ -6,7 +6,7 @@ const logger = debug('redash:http');
|
|||||||
|
|
||||||
function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
|
function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
|
||||||
currentUser, Events, Destination) {
|
currentUser, Events, Destination) {
|
||||||
Events.record(currentUser, 'view', 'page', 'admin/destination');
|
Events.record('view', 'page', 'admin/destination');
|
||||||
$scope.$parent.pageTitle = 'Destinations';
|
$scope.$parent.pageTitle = 'Destinations';
|
||||||
|
|
||||||
$scope.destinationId = $routeParams.destinationId;
|
$scope.destinationId = $routeParams.destinationId;
|
||||||
@ -24,7 +24,7 @@ function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.delete = () => {
|
$scope.delete = () => {
|
||||||
Events.record(currentUser, 'delete', 'destination', $scope.destination.id);
|
Events.record('delete', 'destination', $scope.destination.id);
|
||||||
|
|
||||||
$scope.destination.$delete(() => {
|
$scope.destination.$delete(() => {
|
||||||
toastr.success('Destination deleted successfully.');
|
toastr.success('Destination deleted successfully.');
|
||||||
|
@ -3,7 +3,7 @@ import template from './data-sources.html';
|
|||||||
|
|
||||||
function GroupDataSourcesCtrl($scope, $routeParams, $http, $location, toastr,
|
function GroupDataSourcesCtrl($scope, $routeParams, $http, $location, toastr,
|
||||||
currentUser, Events, Group, DataSource) {
|
currentUser, Events, Group, DataSource) {
|
||||||
Events.record(currentUser, 'view', 'group_data_sources', $scope.groupId);
|
Events.record('view', 'group_data_sources', $scope.groupId);
|
||||||
$scope.group = Group.get({ id: $routeParams.groupId });
|
$scope.group = Group.get({ id: $routeParams.groupId });
|
||||||
$scope.dataSources = Group.dataSources({ id: $routeParams.groupId });
|
$scope.dataSources = Group.dataSources({ id: $routeParams.groupId });
|
||||||
$scope.newDataSource = {};
|
$scope.newDataSource = {};
|
||||||
|
@ -2,7 +2,7 @@ import { Paginator } from '../../utils';
|
|||||||
import template from './list.html';
|
import template from './list.html';
|
||||||
|
|
||||||
function GroupsCtrl($scope, $location, $uibModal, toastr, currentUser, Events, Group) {
|
function GroupsCtrl($scope, $location, $uibModal, toastr, currentUser, Events, Group) {
|
||||||
Events.record(currentUser, 'view', 'page', 'groups');
|
Events.record('view', 'page', 'groups');
|
||||||
// $scope.$parent.pageTitle = 'Groups';
|
// $scope.$parent.pageTitle = 'Groups';
|
||||||
|
|
||||||
$scope.currentUser = currentUser;
|
$scope.currentUser = currentUser;
|
||||||
|
@ -3,7 +3,7 @@ import template from './show.html';
|
|||||||
|
|
||||||
function GroupCtrl($scope, $routeParams, $http, $location, toastr,
|
function GroupCtrl($scope, $routeParams, $http, $location, toastr,
|
||||||
currentUser, Events, Group, User) {
|
currentUser, Events, Group, User) {
|
||||||
Events.record(currentUser, 'view', 'group', $scope.groupId);
|
Events.record('view', 'group', $scope.groupId);
|
||||||
|
|
||||||
$scope.currentUser = currentUser;
|
$scope.currentUser = currentUser;
|
||||||
$scope.group = Group.get({ id: $routeParams.groupId });
|
$scope.group = Group.get({ id: $routeParams.groupId });
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import template from './home.html';
|
import template from './home.html';
|
||||||
|
|
||||||
function HomeCtrl($scope, $uibModal, currentUser, Events, Dashboard, Query) {
|
function HomeCtrl($scope, $uibModal, currentUser, Events, Dashboard, Query) {
|
||||||
Events.record(currentUser, 'view', 'page', 'personal_homepage');
|
Events.record('view', 'page', 'personal_homepage');
|
||||||
// $scope.$parent.pageTitle = 'Home';
|
// $scope.$parent.pageTitle = 'Home';
|
||||||
|
|
||||||
// todo: maybe this should come from some serivce as we have this logic elsewhere.
|
// todo: maybe this should come from some serivce as we have this logic elsewhere.
|
||||||
|
@ -27,7 +27,7 @@ function QuerySearchCtrl($location, $filter, currentUser, Events, Query) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Events.record(currentUser, 'search', 'query', '', { term: this.term });
|
Events.record('search', 'query', '', { term: this.term });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (ngModule) {
|
export default function (ngModule) {
|
||||||
|
@ -9,7 +9,7 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
|||||||
// Obviously it shouldn't be repeated, but we got bigger fish to fry.
|
// Obviously it shouldn't be repeated, but we got bigger fish to fry.
|
||||||
const DEFAULT_TAB = 'table';
|
const DEFAULT_TAB = 'table';
|
||||||
|
|
||||||
Events.record(currentUser, 'view_source', 'query', $scope.query.id);
|
Events.record('view_source', 'query', $scope.query.id);
|
||||||
|
|
||||||
const isNewQuery = !$scope.query.id;
|
const isNewQuery = !$scope.query.id;
|
||||||
let queryText = $scope.query.query;
|
let queryText = $scope.query.query;
|
||||||
@ -74,7 +74,7 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.duplicateQuery = () => {
|
$scope.duplicateQuery = () => {
|
||||||
Events.record(currentUser, 'fork', 'query', $scope.query.id);
|
Events.record('fork', 'query', $scope.query.id);
|
||||||
$scope.query.name = `Copy of (#${$scope.query.id}) ${$scope.query.name}`;
|
$scope.query.name = `Copy of (#${$scope.query.id}) ${$scope.query.name}`;
|
||||||
$scope.query.id = null;
|
$scope.query.id = null;
|
||||||
$scope.query.schedule = null;
|
$scope.query.schedule = null;
|
||||||
@ -95,7 +95,7 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
|||||||
const confirm = { class: 'btn-danger', title: 'Delete' };
|
const confirm = { class: 'btn-danger', title: 'Delete' };
|
||||||
|
|
||||||
AlertDialog.open(title, message, confirm).then(() => {
|
AlertDialog.open(title, message, confirm).then(() => {
|
||||||
Events.record(currentUser, 'delete', 'visualization', vis.id);
|
Events.record('delete', 'visualization', vis.id);
|
||||||
|
|
||||||
Visualization.delete(vis, () => {
|
Visualization.delete(vis, () => {
|
||||||
if ($scope.selectedTab === vis.id) {
|
if ($scope.selectedTab === vis.id) {
|
||||||
|
@ -90,7 +90,7 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
|||||||
$scope.showPermissionsControl = clientConfig.showPermissionsControl;
|
$scope.showPermissionsControl = clientConfig.showPermissionsControl;
|
||||||
|
|
||||||
|
|
||||||
Events.record(currentUser, 'view', 'query', $scope.query.id);
|
Events.record('view', 'query', $scope.query.id);
|
||||||
if ($scope.query.hasResult() || $scope.query.paramsRequired()) {
|
if ($scope.query.hasResult() || $scope.query.paramsRequired()) {
|
||||||
getQueryResult();
|
getQueryResult();
|
||||||
}
|
}
|
||||||
@ -156,12 +156,12 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveDescription = () => {
|
$scope.saveDescription = () => {
|
||||||
Events.record(currentUser, 'edit_description', 'query', $scope.query.id);
|
Events.record('edit_description', 'query', $scope.query.id);
|
||||||
$scope.saveQuery(undefined, { description: $scope.query.description });
|
$scope.saveQuery(undefined, { description: $scope.query.description });
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveName = () => {
|
$scope.saveName = () => {
|
||||||
Events.record(currentUser, 'edit_name', 'query', $scope.query.id);
|
Events.record('edit_name', 'query', $scope.query.id);
|
||||||
$scope.saveQuery(undefined, { name: $scope.query.name });
|
$scope.saveQuery(undefined, { name: $scope.query.name });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
|||||||
getQueryResult(0);
|
getQueryResult(0);
|
||||||
$scope.lockButton(true);
|
$scope.lockButton(true);
|
||||||
$scope.cancelling = false;
|
$scope.cancelling = false;
|
||||||
Events.record(currentUser, 'execute', 'query', $scope.query.id);
|
Events.record('execute', 'query', $scope.query.id);
|
||||||
|
|
||||||
Notifications.getPermissions();
|
Notifications.getPermissions();
|
||||||
};
|
};
|
||||||
@ -185,7 +185,7 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
|||||||
$scope.cancelExecution = () => {
|
$scope.cancelExecution = () => {
|
||||||
$scope.cancelling = true;
|
$scope.cancelling = true;
|
||||||
$scope.queryResult.cancelExecution();
|
$scope.queryResult.cancelExecution();
|
||||||
Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id);
|
Events.record('cancel_execute', 'query', $scope.query.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.archiveQuery = () => {
|
$scope.archiveQuery = () => {
|
||||||
@ -206,7 +206,7 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateDataSource = () => {
|
$scope.updateDataSource = () => {
|
||||||
Events.record(currentUser, 'update_data_source', 'query', $scope.query.id);
|
Events.record('update_data_source', 'query', $scope.query.id);
|
||||||
localStorage.lastSelectedDataSourceId = $scope.query.data_source_id;
|
localStorage.lastSelectedDataSourceId = $scope.query.data_source_id;
|
||||||
|
|
||||||
$scope.query.latest_query_data = null;
|
$scope.query.latest_query_data = null;
|
||||||
|
@ -4,7 +4,7 @@ import template from './edit.html';
|
|||||||
function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, Events, QuerySnippet) {
|
function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, Events, QuerySnippet) {
|
||||||
// $scope.$parent.pageTitle = 'Query Snippets';
|
// $scope.$parent.pageTitle = 'Query Snippets';
|
||||||
this.snippetId = $routeParams.snippetId;
|
this.snippetId = $routeParams.snippetId;
|
||||||
Events.record(currentUser, 'view', 'query_snippet', this.snippetId);
|
Events.record('view', 'query_snippet', this.snippetId);
|
||||||
|
|
||||||
this.editorOptions = {
|
this.editorOptions = {
|
||||||
mode: 'snippets',
|
mode: 'snippets',
|
||||||
|
@ -2,7 +2,7 @@ import { Paginator } from '../../utils';
|
|||||||
import template from './list.html';
|
import template from './list.html';
|
||||||
|
|
||||||
function SnippetsCtrl($location, currentUser, Events, QuerySnippet) {
|
function SnippetsCtrl($location, currentUser, Events, QuerySnippet) {
|
||||||
Events.record(currentUser, 'view', 'page', 'query_snippets');
|
Events.record('view', 'page', 'query_snippets');
|
||||||
// $scope.$parent.pageTitle = 'Query Snippets';
|
// $scope.$parent.pageTitle = 'Query Snippets';
|
||||||
|
|
||||||
this.snippets = new Paginator([], { itemsPerPage: 20 });
|
this.snippets = new Paginator([], { itemsPerPage: 20 });
|
||||||
|
@ -2,7 +2,7 @@ import { Paginator } from '../../utils';
|
|||||||
import template from './list.html';
|
import template from './list.html';
|
||||||
|
|
||||||
function UsersCtrl($location, toastr, currentUser, Events, User) {
|
function UsersCtrl($location, toastr, currentUser, Events, User) {
|
||||||
Events.record(currentUser, 'view', 'page', 'users');
|
Events.record('view', 'page', 'users');
|
||||||
// $scope.$parent.pageTitle = 'Users';
|
// $scope.$parent.pageTitle = 'Users';
|
||||||
|
|
||||||
this.currentUser = currentUser;
|
this.currentUser = currentUser;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import template from './new.html';
|
import template from './new.html';
|
||||||
|
|
||||||
function NewUserCtrl($scope, $location, toastr, currentUser, Events, User) {
|
function NewUserCtrl($scope, $location, toastr, currentUser, Events, User) {
|
||||||
Events.record(currentUser, 'view', 'page', 'users/new');
|
Events.record('view', 'page', 'users/new');
|
||||||
|
|
||||||
$scope.user = new User({});
|
$scope.user = new User({});
|
||||||
$scope.saveUser = () => {
|
$scope.saveUser = () => {
|
||||||
|
@ -13,7 +13,7 @@ function UserCtrl($scope, $routeParams, $http, $location, toastr,
|
|||||||
$scope.userId = currentUser.id;
|
$scope.userId = currentUser.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Events.record(currentUser, 'view', 'user', $scope.userId);
|
Events.record('view', 'user', $scope.userId);
|
||||||
$scope.canEdit = currentUser.hasPermission('admin') || currentUser.id === parseInt($scope.userId, 10);
|
$scope.canEdit = currentUser.hasPermission('admin') || currentUser.id === parseInt($scope.userId, 10);
|
||||||
$scope.showSettings = false;
|
$scope.showSettings = false;
|
||||||
$scope.showPasswordSettings = false;
|
$scope.showPasswordSettings = false;
|
||||||
|
56
frontend/app/services/auth.js
Normal file
56
frontend/app/services/auth.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
function getLocalSessionData() {
|
||||||
|
const sessionData = window.sessionStorage.getItem('session');
|
||||||
|
if (sessionData) {
|
||||||
|
return JSON.parse(sessionData);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthService($window, $location, $q, $http) {
|
||||||
|
const Auth = {
|
||||||
|
isAuthenticated() {
|
||||||
|
return getLocalSessionData() !== null;
|
||||||
|
},
|
||||||
|
login() {
|
||||||
|
// const next = encodeURI($location.url());
|
||||||
|
console.log('do the login manually!');
|
||||||
|
// $window.location.href = `http://localhost:5000/default/login?next=${next}`;
|
||||||
|
},
|
||||||
|
loadSession() {
|
||||||
|
const sessionData = getLocalSessionData();
|
||||||
|
if (sessionData) {
|
||||||
|
return $q.resolve(sessionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $http.get('/api/session').then((response) => {
|
||||||
|
window.sessionStorage.setItem('session', JSON.stringify(response.data));
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CurrentUserService() {
|
||||||
|
Object.assign(this, getLocalSessionData().user);
|
||||||
|
|
||||||
|
this.canEdit = (object) => {
|
||||||
|
const userId = object.user_id || (object.user && object.user.id);
|
||||||
|
return this.hasPermission('admin') || (userId && (userId === this.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.hasPermission = permission => this.permissions.indexOf(permission) !== -1;
|
||||||
|
|
||||||
|
this.isAdmin = this.hasPermission('admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClientConfigService() {
|
||||||
|
Object.assign(this, getLocalSessionData().client_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (ngModule) {
|
||||||
|
ngModule.factory('Auth', AuthService);
|
||||||
|
ngModule.service('currentUser', CurrentUserService);
|
||||||
|
ngModule.service('clientConfig', ClientConfigService);
|
||||||
|
}
|
@ -10,9 +10,8 @@ function Events($http) {
|
|||||||
$http.post('api/events', events);
|
$http.post('api/events', events);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
this.record = function record(user, action, objectType, objectId, additionalProperties) {
|
this.record = function record(action, objectType, objectId, additionalProperties) {
|
||||||
const event = {
|
const event = {
|
||||||
user_id: user.id,
|
|
||||||
action,
|
action,
|
||||||
object_type: objectType,
|
object_type: objectType,
|
||||||
object_id: objectId,
|
object_id: objectId,
|
||||||
|
@ -13,3 +13,4 @@ export { default as QuerySnippet } from './query-snippet';
|
|||||||
export { default as Notifications } from './notifications';
|
export { default as Notifications } from './notifications';
|
||||||
export { default as KeyboardShortcuts } from './keyboard-shortcuts';
|
export { default as KeyboardShortcuts } from './keyboard-shortcuts';
|
||||||
export { default as AlertDialog } from './alert-dialog';
|
export { default as AlertDialog } from './alert-dialog';
|
||||||
|
export { default as Auth } from './auth';
|
||||||
|
@ -70,7 +70,7 @@ function Notifications(currentUser, Events) {
|
|||||||
notification.onclick = function onClick() {
|
notification.onclick = function onClick() {
|
||||||
window.focus();
|
window.focus();
|
||||||
this.close();
|
this.close();
|
||||||
Events.record(currentUser, 'click', 'notification');
|
Events.record('click', 'notification');
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,9 +47,9 @@ const EditVisualizationDialog = {
|
|||||||
|
|
||||||
this.submit = () => {
|
this.submit = () => {
|
||||||
if (this.visualization.id) {
|
if (this.visualization.id) {
|
||||||
Events.record(currentUser, 'update', 'visualization', this.visualization.id, { type: this.visualization.type });
|
Events.record('update', 'visualization', this.visualization.id, { type: this.visualization.type });
|
||||||
} else {
|
} else {
|
||||||
Events.record(currentUser, 'create', 'visualization', null, { type: this.visualization.type });
|
Events.record('create', 'visualization', null, { type: this.visualization.type });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.visualization.query_id = this.query.id;
|
this.visualization.query_id = this.query.id;
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- build:css /styles/main.css -->
|
<!-- build:css /styles/main.css -->
|
||||||
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
|
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
|
||||||
<link rel="stylesheet" href="/styles/redash.css">
|
|
||||||
<!-- endbuild -->
|
<!-- endbuild -->
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||||
@ -72,20 +71,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// TODO: move currentUser & features to be an Angular service
|
// TODO: move currentUser & features to be an Angular service
|
||||||
var clientConfig = {{ client_config|safe }};
|
|
||||||
var basePath = "{{base_href}}";
|
var basePath = "{{base_href}}";
|
||||||
var currentUser = {{ user|safe }};
|
|
||||||
|
|
||||||
currentUser.canEdit = function(object) {
|
|
||||||
var user_id = object.user_id || (object.user && object.user.id);
|
|
||||||
return this.hasPermission('admin') || (user_id && (user_id == currentUser.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
currentUser.hasPermission = function(permission) {
|
|
||||||
return this.permissions.indexOf(permission) != -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
currentUser.isAdmin = currentUser.hasPermission('admin');
|
|
||||||
</script>
|
</script>
|
||||||
{% include '_includes/tail.html' %}
|
{% include '_includes/tail.html' %}
|
||||||
|
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
(function () {
|
|
||||||
var dateFormatter = function (value) {
|
|
||||||
if (!value) {
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.format(clientConfig.dateTimeFormat);
|
|
||||||
};
|
|
||||||
|
|
||||||
var MainCtrl = function ($scope, $location, Dashboard) {
|
|
||||||
$scope.$on("$routeChangeSuccess", function (event, current, previous, rejection) {
|
|
||||||
if ($scope.showPermissionError) {
|
|
||||||
$scope.showPermissionError = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$on("$routeChangeError", function (event, current, previous, rejection) {
|
|
||||||
if (rejection.status === 403) {
|
|
||||||
$scope.showPermissionError = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.location = String(document.location);
|
|
||||||
$scope.version = clientConfig.version;
|
|
||||||
$scope.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.hasPermission("admin");
|
|
||||||
};
|
|
||||||
|
|
||||||
angular.module('redash.controllers', [])
|
|
||||||
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', IndexCtrl])
|
|
||||||
.controller('MainCtrl', ['$scope', '$location', 'Dashboard', MainCtrl])
|
|
||||||
.controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl])
|
|
||||||
})();
|
|
@ -1,18 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
from flask import current_app
|
|
||||||
from flask_login import login_required
|
|
||||||
|
|
||||||
|
from flask_login import login_required
|
||||||
from redash import models, redis_connection
|
from redash import models, redis_connection
|
||||||
from redash.utils import json_dumps
|
|
||||||
from redash.handlers import routes
|
from redash.handlers import routes
|
||||||
|
from redash.handlers.base import json_response
|
||||||
from redash.permissions import require_super_admin
|
from redash.permissions import require_super_admin
|
||||||
from redash.tasks.queries import QueryTaskTracker
|
from redash.tasks.queries import QueryTaskTracker
|
||||||
|
|
||||||
|
|
||||||
def json_response(response):
|
|
||||||
return current_app.response_class(json_dumps(response), mimetype='application/json')
|
|
||||||
|
|
||||||
|
|
||||||
@routes.route('/api/admin/queries/outdated', methods=['GET'])
|
@routes.route('/api/admin/queries/outdated', methods=['GET'])
|
||||||
@require_super_admin
|
@require_super_admin
|
||||||
@login_required
|
@login_required
|
||||||
@ -45,4 +40,3 @@ def queries_tasks():
|
|||||||
}
|
}
|
||||||
|
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from flask import render_template, request, redirect, url_for, flash
|
|
||||||
from flask_login import current_user, login_user, logout_user
|
|
||||||
|
|
||||||
from redash import models, settings, limiter
|
from flask import flash, redirect, render_template, request, url_for
|
||||||
from redash.handlers import routes
|
from flask_login import current_user, login_user, logout_user
|
||||||
from redash.handlers.base import org_scoped_rule
|
from redash import __version__, models, settings, limiter
|
||||||
from redash.authentication import current_org, get_login_url
|
from redash.authentication import current_org, get_login_url
|
||||||
from redash.authentication.account import validate_token, BadSignature, SignatureExpired, send_password_reset_email
|
from redash.authentication.account import (BadSignature, SignatureExpired,
|
||||||
|
send_password_reset_email,
|
||||||
|
validate_token)
|
||||||
|
from redash.handlers import routes
|
||||||
|
from redash.handlers.base import json_response, org_scoped_rule
|
||||||
|
from redash.version_check import get_latest_version
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -125,3 +129,31 @@ def login(org_slug=None):
|
|||||||
def logout(org_slug=None):
|
def logout(org_slug=None):
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(get_login_url(next=None))
|
return redirect(get_login_url(next=None))
|
||||||
|
|
||||||
|
|
||||||
|
@routes.route(org_scoped_rule('/api/session'), methods=['GET'])
|
||||||
|
def session(org_slug=None):
|
||||||
|
email_md5 = hashlib.md5(current_user.email.lower()).hexdigest()
|
||||||
|
gravatar_url = "https://www.gravatar.com/avatar/%s?s=40" % email_md5
|
||||||
|
|
||||||
|
user = {
|
||||||
|
'gravatar_url': gravatar_url,
|
||||||
|
'id': current_user.id,
|
||||||
|
'name': current_user.name,
|
||||||
|
'email': current_user.email,
|
||||||
|
'groups': current_user.groups,
|
||||||
|
'permissions': current_user.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
client_config = {
|
||||||
|
'newVersionAvailable': get_latest_version(),
|
||||||
|
'version': __version__
|
||||||
|
}
|
||||||
|
|
||||||
|
client_config.update(settings.COMMON_CLIENT_CONFIG)
|
||||||
|
|
||||||
|
return json_response({
|
||||||
|
'user': user,
|
||||||
|
'org_slug': current_org.slug,
|
||||||
|
'client_config': client_config
|
||||||
|
})
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import time
|
import time
|
||||||
from flask import request, Blueprint
|
|
||||||
from flask_restful import Resource, abort
|
|
||||||
from flask_login import current_user, login_required
|
|
||||||
from peewee import DoesNotExist
|
|
||||||
|
|
||||||
|
from flask import Blueprint, current_app, request
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from flask_restful import Resource, abort
|
||||||
|
from peewee import DoesNotExist
|
||||||
from redash import settings
|
from redash import settings
|
||||||
from redash.tasks import record_event as record_event_task
|
|
||||||
from redash.models import ApiUser
|
|
||||||
from redash.authentication import current_org
|
from redash.authentication import current_org
|
||||||
|
from redash.models import ApiUser
|
||||||
|
from redash.tasks import record_event as record_event_task
|
||||||
|
from redash.utils import json_dumps
|
||||||
|
|
||||||
routes = Blueprint('redash', __name__, template_folder=settings.fix_assets_path('templates'))
|
routes = Blueprint('redash', __name__, template_folder=settings.fix_assets_path('templates'))
|
||||||
|
|
||||||
@ -99,3 +100,7 @@ def org_scoped_rule(rule):
|
|||||||
return "/<org_slug:org_slug>{}".format(rule)
|
return "/<org_slug:org_slug>{}".format(rule)
|
||||||
|
|
||||||
return rule
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def json_response(response):
|
||||||
|
return current_app.response_class(json_dumps(response), mimetype='application/json')
|
||||||
|
@ -117,10 +117,6 @@ class QueryResource(BaseResource):
|
|||||||
except models.ConflictDetectedError:
|
except models.ConflictDetectedError:
|
||||||
abort(409)
|
abort(409)
|
||||||
|
|
||||||
# old_query = copy.deepcopy(query.to_dict())
|
|
||||||
# new_change = query.update_instance_tracked(changing_user=self.current_user, old_object=old_query, **query_def)
|
|
||||||
# abort(409) # HTTP 'Conflict' status code
|
|
||||||
|
|
||||||
result = query.to_dict(with_visualizations=True)
|
result = query.to_dict(with_visualizations=True)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user