refactor: Removes the Profile feature (#26462)

This commit is contained in:
Michael S. Molina 2024-01-18 15:36:40 -03:00 committed by GitHub
parent b245e66198
commit 8f8c435d7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 8 additions and 1508 deletions

View File

@ -77,7 +77,6 @@
| can select star on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can warm up cache on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can sqllab table viz on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| can profile on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can available domains on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can request access on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |
| can dashboard on Superset | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | O |

View File

@ -39,6 +39,7 @@ assists people when migrating to a new version.
- [26331](https://github.com/apache/superset/issues/26331): Removes the deprecated `DISABLE_DATASET_SOURCE_EDIT` feature flag. The previous value of the feature flag was `False` and now the feature is permanently removed.
- [26636](https://github.com/apache/superset/issues/26636): Sets the `DASHBOARD_VIRTUALIZATION` feature flag to `True` by default. This feature was introduced by [21438](https://github.com/apache/superset/pull/21438) and will enable virtualization when rendering a dashboard's charts in an attempt to reduce the number of elements (DOM nodes) rendered at once. This is especially useful for large dashboards.
- [26637](https://github.com/apache/superset/issues/26637): Sets the `DRILL_BY` feature flag to `True` by default given that the feature has been tested for a while and reached a stable state.
- [26462](https://github.com/apache/superset/issues/26462): Removes the Profile feature given that it's not actively maintained and not widely used.
### Potential Downtime

View File

@ -112,7 +112,6 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^16.13.0",
"react-draggable": "^4.4.6",
"react-gravatar": "^2.6.1",
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.4.1",
"react-js-cron": "^1.2.0",
@ -25235,14 +25234,6 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"devOptional": true
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
"engines": {
"node": "*"
}
},
"node_modules/check-more-types": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
@ -27066,14 +27057,6 @@
"node": ">=4.8"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
"engines": {
"node": "*"
}
},
"node_modules/crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -36753,11 +36736,6 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"node_modules/is-retina": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
"integrity": "sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M="
},
"node_modules/is-scoped": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-2.1.0.tgz",
@ -44884,16 +44862,6 @@
"integrity": "sha512-QBcepxkFxuGk12q4G0KuNbuU3UCXhDROxWZllaNZSpBivkHl2z8qNvi7UGE/WLJt+c7GTC4jigYtur+JDL+40A==",
"deprecated": "Use cephes instead, for a more complete and well-tested module"
},
"node_modules/md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
"dependencies": {
"charenc": "~0.0.1",
"crypt": "~0.0.1",
"is-buffer": "~1.1.1"
}
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -51757,39 +51725,6 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"node_modules/react-gravatar": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/react-gravatar/-/react-gravatar-2.6.3.tgz",
"integrity": "sha1-VAfrash+gw4qNN63YNKkxATrHaw=",
"dependencies": {
"is-retina": "^1.0.3",
"md5": "^2.1.0",
"query-string": "^4.2.2"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-gravatar/node_modules/query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
"dependencies": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-gravatar/node_modules/strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-helmet-async": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
@ -84150,11 +84085,6 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"devOptional": true
},
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
"check-more-types": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
@ -85609,11 +85539,6 @@
"which": "^1.2.9"
}
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -92968,11 +92893,6 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-retina": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
"integrity": "sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M="
},
"is-scoped": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-2.1.0.tgz",
@ -99270,16 +99190,6 @@
"resolved": "https://registry.npmjs.org/mathfn/-/mathfn-1.2.0.tgz",
"integrity": "sha512-QBcepxkFxuGk12q4G0KuNbuU3UCXhDROxWZllaNZSpBivkHl2z8qNvi7UGE/WLJt+c7GTC4jigYtur+JDL+40A=="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
"requires": {
"charenc": "~0.0.1",
"crypt": "~0.0.1",
"is-buffer": "~1.1.1"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -104452,32 +104362,6 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-gravatar": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/react-gravatar/-/react-gravatar-2.6.3.tgz",
"integrity": "sha1-VAfrash+gw4qNN63YNKkxATrHaw=",
"requires": {
"is-retina": "^1.0.3",
"md5": "^2.1.0",
"query-string": "^4.2.2"
},
"dependencies": {
"query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
"requires": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
}
},
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
}
}
},
"react-helmet-async": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",

View File

@ -178,7 +178,6 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^16.13.0",
"react-draggable": "^4.4.6",
"react-gravatar": "^2.6.1",
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.4.1",
"react-js-cron": "^1.2.0",

View File

@ -1,122 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { Provider } from 'react-redux';
import fetchMock from 'fetch-mock';
import { storeWithState } from 'spec/fixtures/mockStore';
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
import TableLoader, { TableLoaderProps } from '.';
const NO_DATA_TEXT = 'No data available';
const MOCK_GLOB = 'glob:*/api/v1/mock';
fetchMock.get(MOCK_GLOB, [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
]);
const defaultProps: TableLoaderProps = {
dataEndpoint: '/api/v1/mock',
addDangerToast: jest.fn(),
noDataText: NO_DATA_TEXT,
};
function renderWithProps(props: TableLoaderProps = defaultProps) {
return render(
<Provider store={storeWithState({})}>
<TableLoader {...props} />
<ToastContainer />
</Provider>,
);
}
test('renders loading and table', async () => {
renderWithProps();
expect(screen.getByRole('status')).toBeInTheDocument();
expect(await screen.findByRole('table')).toBeInTheDocument();
});
test('renders with column names', async () => {
renderWithProps({
...defaultProps,
columns: ['id_modified', 'name_modified'],
});
const columnHeaders = await screen.findAllByRole('columnheader');
expect(columnHeaders[0]).toHaveTextContent('id_modified');
expect(columnHeaders[1]).toHaveTextContent('name_modified');
});
test('renders without mutator', async () => {
renderWithProps();
expect(await screen.findAllByRole('row')).toHaveLength(3);
expect(await screen.findAllByRole('columnheader')).toHaveLength(2);
expect(await screen.findAllByRole('cell')).toHaveLength(4);
});
test('renders with mutator', async () => {
const mutator = function (data: { id: number; name: string }[]) {
return data.map(row => ({
id: row.id,
name: <h4>{row.name}</h4>,
}));
};
renderWithProps({ ...defaultProps, mutator });
expect(await screen.findAllByRole('heading', { level: 4 })).toHaveLength(2);
});
test('renders empty message', async () => {
fetchMock.mock(MOCK_GLOB, [], {
overwriteRoutes: true,
});
renderWithProps();
expect(await screen.findByText('No data available')).toBeInTheDocument();
});
test('renders blocked message', async () => {
fetchMock.mock(MOCK_GLOB, 403, {
overwriteRoutes: true,
});
renderWithProps();
expect(
await screen.findByText('Access to user activity data is restricted'),
).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
test('renders error message', async () => {
fetchMock.mock(MOCK_GLOB, 500, {
overwriteRoutes: true,
});
renderWithProps();
expect(await screen.findByText(NO_DATA_TEXT)).toBeInTheDocument();
expect(await screen.findByRole('alert')).toBeInTheDocument();
});

View File

@ -1,98 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect, useMemo } from 'react';
import { t, SupersetClient, JsonObject } from '@superset-ui/core';
import TableView, { EmptyWrapperType } from 'src/components/TableView';
import withToasts from 'src/components/MessageToasts/withToasts';
import Loading from 'src/components/Loading';
import '../../assets/stylesheets/reactable-pagination.less';
export interface TableLoaderProps {
dataEndpoint?: string;
mutator?: (data: JsonObject) => any[];
columns?: string[];
noDataText?: string;
addDangerToast(text: string): any;
}
const TableLoader = (props: TableLoaderProps) => {
const [data, setData] = useState<Array<any>>([]);
const [isLoading, setIsLoading] = useState(true);
const [isBlocked, setIsBlocked] = useState(false);
useEffect(() => {
const { dataEndpoint, mutator } = props;
if (dataEndpoint) {
SupersetClient.get({ endpoint: dataEndpoint })
.then(({ json }) => {
const data = (mutator ? mutator(json) : json) as Array<any>;
setData(data);
setIsBlocked(false);
setIsLoading(false);
})
.catch(response => {
setIsLoading(false);
if (response.status === 403) {
setIsBlocked(true);
} else {
setIsBlocked(false);
props.addDangerToast(t('An error occurred'));
}
});
}
}, [props]);
const { columns, noDataText, ...tableProps } = props;
const memoizedColumns = useMemo(() => {
let tableColumns = columns;
if (!columns && data.length > 0) {
tableColumns = Object.keys(data[0]).filter(col => col[0] !== '_');
}
return tableColumns
? tableColumns.map((column: string) => ({
accessor: column,
Header: column,
}))
: [];
}, [columns, data]);
delete tableProps.dataEndpoint;
delete tableProps.mutator;
if (isLoading) {
return <Loading />;
}
return (
<TableView
columns={memoizedColumns}
data={data}
pageSize={50}
loading={isLoading}
emptyWrapperType={EmptyWrapperType.Small}
noDataText={
isBlocked ? t('Access to user activity data is restricted') : noDataText
}
{...tableProps}
/>
);
};
export default withToasts(TableLoader);

View File

@ -175,7 +175,6 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
user_info_url: '',
user_login_url: '',
user_logout_url: '',
user_profile_url: '',
locale: '',
},
settings: [],

View File

@ -171,7 +171,7 @@ const mockedProps = {
},
],
brand: {
path: '/superset/profile/admin/',
path: '/superset/welcome/',
icon: '/static/assets/images/superset-logo-horiz.png',
alt: 'Superset',
width: '126',
@ -203,7 +203,6 @@ const mockedProps = {
user_info_url: '/users/userinfo/',
user_logout_url: '/logout/',
user_login_url: '/login/',
user_profile_url: '/profile/',
locale: 'en',
version_string: '1.0.0',
version_sha: 'randomSHA',
@ -464,25 +463,6 @@ test('should NOT render the user actions when user is anonymous', async () => {
expect(screen.queryByText('User')).not.toBeInTheDocument();
});
test('should render the Profile link when available', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
const {
data: {
navbar_right: { user_profile_url },
},
} = mockedProps;
render(<Menu {...notanonProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
});
userEvent.hover(screen.getByText('Settings'));
const profile = await screen.findByText('Profile');
expect(profile).toHaveAttribute('href', user_profile_url);
});
test('should render the About section and version_string, sha or build_number when available', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
const {

View File

@ -117,7 +117,6 @@ const createProps = (): RightMenuProps => ({
user_info_url: '/users/userinfo/',
user_logout_url: '/logout/',
user_login_url: '/login/',
user_profile_url: '/profile/',
locale: 'en',
version_string: '1.0.0',
version_sha: 'randomSHA',

View File

@ -473,11 +473,6 @@ const RightMenu = ({
{!navbarRight.user_is_anonymous && [
<Menu.Divider key="user-divider" />,
<Menu.ItemGroup key="user-section" title={t('User')}>
{navbarRight.user_profile_url && (
<Menu.Item key="profile">
<Link to={navbarRight.user_profile_url}>{t('Profile')}</Link>
</Menu.Item>
)}
{navbarRight.user_info_url && (
<Menu.Item key="info">
<a href={navbarRight.user_info_url}>{t('Info')}</a>

View File

@ -1,50 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import TableLoader from 'src/components/TableLoader';
import CreatedContent from './CreatedContent';
import { user } from './fixtures';
// store needed for withToasts(TableLoader)
const mockStore = configureStore([thunk]);
const store = mockStore({});
describe('CreatedContent', () => {
const mockedProps = {
user,
};
it('renders 2 TableLoader', () => {
const wrapper = shallow(<CreatedContent {...mockedProps} />, {
context: { store },
});
expect(wrapper.find(TableLoader)).toHaveLength(2);
});
it('renders 2 titles', () => {
const wrapper = shallow(<CreatedContent {...mockedProps} />, {
context: { store },
});
expect(wrapper.find('h3')).toHaveLength(2);
});
});

View File

@ -1,111 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import rison from 'rison';
import React from 'react';
import { t } from '@superset-ui/core';
import TableLoader from 'src/components/TableLoader';
import {
BootstrapUser,
ChartResponse,
DashboardResponse,
} from 'src/types/bootstrapTypes';
interface CreatedContentProps {
user: BootstrapUser;
}
class CreatedContent extends React.PureComponent<CreatedContentProps> {
renderSliceTable() {
const search = [
{ col: 'created_by', opr: 'chart_created_by_me', value: 'me' },
];
const query = rison.encode({
keys: ['none'],
columns: ['created_on_delta_humanized', 'slice_name', 'url'],
filters: search,
order_column: 'changed_on_delta_humanized',
order_direction: 'desc',
page: 0,
page_size: 100,
});
const mutator = (data: ChartResponse) =>
data.result.map(chart => ({
chart: <a href={chart.url}>{chart.slice_name}</a>,
created: chart.created_on_delta_humanized,
_created: chart.created_on_delta_humanized,
}));
return (
<TableLoader
dataEndpoint={`/api/v1/chart/?q=${query}`}
className="table-condensed"
columns={['chart', 'created']}
mutator={mutator}
noDataText={t('No charts')}
sortable
/>
);
}
renderDashboardTable() {
const search = [
{ col: 'created_by', opr: 'dashboard_created_by_me', value: 'me' },
];
const query = rison.encode({
keys: ['none'],
columns: ['created_on_delta_humanized', 'dashboard_title', 'url'],
filters: search,
order_column: 'changed_on',
order_direction: 'desc',
page: 0,
page_size: 100,
});
const mutator = (data: DashboardResponse) =>
data.result.map(dash => ({
dashboard: <a href={dash.url}>{dash.dashboard_title}</a>,
created: dash.created_on_delta_humanized,
_created: dash.created_on_delta_humanized,
}));
return (
<TableLoader
className="table-condensed"
mutator={mutator}
dataEndpoint={`/api/v1/dashboard/?q=${query}`}
noDataText={t('No dashboards')}
columns={['dashboard', 'created']}
sortable
/>
);
}
render() {
return (
<div>
<h3>{t('Dashboards')}</h3>
{this.renderDashboardTable()}
<hr />
<h3>{t('Charts')}</h3>
{this.renderSliceTable()}
</div>
);
}
}
export default CreatedContent;

View File

@ -1,50 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import TableLoader from 'src/components/TableLoader';
import Favorites from './Favorites';
import { user } from './fixtures';
// store needed for withToasts(TableLoader)
const mockStore = configureStore([thunk]);
const store = mockStore({});
describe('Favorites', () => {
const mockedProps = {
user,
};
it('renders 2 TableLoader', () => {
const wrapper = shallow(<Favorites {...mockedProps} />, {
context: { store },
});
expect(wrapper.find(TableLoader)).toHaveLength(2);
});
it('renders 2 titles', () => {
const wrapper = shallow(<Favorites {...mockedProps} />, {
context: { store },
});
expect(wrapper.find('h3')).toHaveLength(2);
});
});

View File

@ -1,107 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import rison from 'rison';
import moment from 'moment';
import { t } from '@superset-ui/core';
import { DashboardResponse, BootstrapUser } from 'src/types/bootstrapTypes';
import TableLoader from '../../components/TableLoader';
import { Chart } from './types';
interface FavoritesProps {
user: BootstrapUser;
}
export default class Favorites extends React.PureComponent<FavoritesProps> {
renderSliceTable() {
const mutator = (payload: { result: Chart[] }) =>
payload.result.map(slice => ({
slice: <a href={slice.slice_url}>{slice.slice_name}</a>,
creator: slice.created_by_name,
favorited: moment.utc(slice.changed_on_dttm).fromNow(),
_favorited: slice.changed_on_dttm,
}));
const query = rison.encode({
filters: [
{
col: 'id',
opr: 'chart_is_favorite',
value: true,
},
],
order_column: 'slice_name',
order_direction: 'asc',
page: 0,
page_size: 25,
});
return (
<TableLoader
dataEndpoint={`/api/v1/chart/?q=${query}`}
className="table-condensed"
columns={['slice', 'creator', 'favorited']}
mutator={mutator}
noDataText={t('No favorite charts yet, go click on stars!')}
sortable
/>
);
}
renderDashboardTable() {
const search = [{ col: 'id', opr: 'dashboard_is_favorite', value: true }];
const query = rison.encode({
keys: ['none'],
columns: ['created_on_delta_humanized', 'dashboard_title', 'url'],
filters: search,
order_column: 'changed_on',
order_direction: 'desc',
page: 0,
page_size: 100,
});
const mutator = (data: DashboardResponse) =>
data.result.map(dash => ({
dashboard: <a href={dash.url}>{dash.dashboard_title}</a>,
created: dash.created_on_delta_humanized,
_created: dash.created_on_delta_humanized,
}));
return (
<TableLoader
className="table-condensed"
mutator={mutator}
dataEndpoint={`/api/v1/dashboard/?q=${query}`}
noDataText={t('No favorite dashboards yet, go click on stars!')}
columns={['dashboard', 'creator', 'created']}
sortable
/>
);
}
render() {
return (
<div>
<h3>{t('Dashboards')}</h3>
{this.renderDashboardTable()}
<hr />
<h3>{t('Charts')}</h3>
{this.renderSliceTable()}
</div>
);
}
}

View File

@ -1,40 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import TableLoader from 'src/components/TableLoader';
import RecentActivity from './RecentActivity';
import { user } from './fixtures';
describe('RecentActivity', () => {
const mockedProps = {
user,
};
it('is valid', () => {
expect(React.isValidElement(<RecentActivity {...mockedProps} />)).toBe(
true,
);
});
it('renders a TableLoader', () => {
const wrapper = shallow(<RecentActivity {...mockedProps} />);
expect(wrapper.find(TableLoader)).toExist();
});
});

View File

@ -1,55 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import moment from 'moment';
import { t } from '@superset-ui/core';
import rison from 'rison';
import TableLoader from 'src/components/TableLoader';
import { BootstrapUser } from 'src/types/bootstrapTypes';
import { ActivityResult } from './types';
interface RecentActivityProps {
user: BootstrapUser;
}
export default function RecentActivity({ user }: RecentActivityProps) {
const rowLimit = 50;
const mutator = function (data: ActivityResult) {
return data.result
.filter(row => row.action === 'dashboard' || row.action === 'explore')
.map(row => ({
name: <a href={row.item_url}>{row.item_title}</a>,
type: row.action,
time: moment.utc(row.time).fromNow(),
_time: row.time,
}));
};
const params = rison.encode({ page_size: rowLimit });
return (
<div>
<TableLoader
className="table-condensed"
mutator={mutator}
sortable
dataEndpoint={`/api/v1/log/recent_activity/?q=${params}`}
noDataText={t('No Data')}
/>
</div>
);
}

View File

@ -1,49 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { styledMount as mount } from 'spec/helpers/theming';
import Label from 'src/components/Label';
import { user, userNoPerms } from './fixtures';
import Security from './Security';
describe('Security', () => {
const mockedProps = {
user,
};
it('is valid', () => {
expect(React.isValidElement(<Security {...mockedProps} />)).toBe(true);
});
it('renders 2 role labels', () => {
const wrapper = mount(<Security {...mockedProps} />);
expect(wrapper.find('.roles').find(Label)).toHaveLength(2);
});
it('renders 2 datasource labels', () => {
const wrapper = mount(<Security {...mockedProps} />);
expect(wrapper.find('.datasources').find(Label)).toHaveLength(2);
});
it('renders 3 database labels', () => {
const wrapper = mount(<Security {...mockedProps} />);
expect(wrapper.find('.databases').find(Label)).toHaveLength(3);
});
it('renders no permission label when empty', () => {
const wrapper = mount(<Security user={userNoPerms} />);
expect(wrapper.find('.datasources').find(Label)).not.toExist();
expect(wrapper.find('.databases').find(Label)).not.toExist();
});
});

View File

@ -1,75 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import Badge from 'src/components/Badge';
import { t } from '@superset-ui/core';
import Label from 'src/components/Label';
import { BootstrapUser } from 'src/types/bootstrapTypes';
interface SecurityProps {
user: BootstrapUser;
}
export default function Security({ user }: SecurityProps) {
return (
<div>
<div className="roles">
<h4>
{t('Roles')}{' '}
<Badge count={Object.keys(user?.roles || {}).length} showZero />
</h4>
{Object.keys(user?.roles || {}).map(role => (
<Label key={role}>{role}</Label>
))}
<hr />
</div>
<div className="databases">
{user?.permissions.database_access && (
<div>
<h4>
{t('Databases')}{' '}
<Badge count={user.permissions.database_access.length} showZero />
</h4>
{user.permissions.database_access.map(role => (
<Label key={role}>{role}</Label>
))}
<hr />
</div>
)}
</div>
<div className="datasources">
{user?.permissions.datasource_access && (
<div>
<h4>
{t('Datasets')}{' '}
<Badge
count={user.permissions.datasource_access.length}
showZero
/>
</h4>
{user.permissions.datasource_access.map(role => (
<Label key={role}>{role}</Label>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -1,53 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import Gravatar from 'react-gravatar';
import { mount } from 'enzyme';
import UserInfo from './UserInfo';
import { user } from './fixtures';
describe('UserInfo', () => {
const mockedProps = {
user,
};
it('is valid', () => {
expect(React.isValidElement(<UserInfo {...mockedProps} />)).toBe(true);
});
it('renders a Gravatar', () => {
const wrapper = mount(<UserInfo {...mockedProps} />);
expect(wrapper.find(Gravatar)).toExist();
});
it('renders a Panel', () => {
const wrapper = mount(<UserInfo {...mockedProps} />);
expect(wrapper.find('.panel')).toExist();
});
it('renders 5 icons', () => {
const wrapper = mount(<UserInfo {...mockedProps} />);
expect(wrapper.find('i')).toHaveLength(5);
});
it('renders roles information', () => {
const wrapper = mount(<UserInfo {...mockedProps} />);
expect(wrapper.find('.roles').text()).toBe(' Alpha, sql_lab');
});
it('shows the right user-id', () => {
const wrapper = mount(<UserInfo {...mockedProps} />);
expect(wrapper.find('.user-id').text()).toBe('5');
});
});

View File

@ -1,82 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import Gravatar from 'react-gravatar';
import moment from 'moment';
import { t, styled } from '@superset-ui/core';
import { BootstrapUser } from 'src/types/bootstrapTypes';
interface UserInfoProps {
user: BootstrapUser;
}
const StyledContainer = styled.div`
.panel {
padding: ${({ theme }) => theme.gridUnit * 6}px;
}
`;
export default function UserInfo({ user }: UserInfoProps) {
return (
<StyledContainer>
<a href="https://en.gravatar.com/">
<Gravatar
email={user?.email}
width="100%"
height=""
size={220}
alt={t('Profile picture provided by Gravatar')}
className="img-rounded"
style={{ borderRadius: 15 }}
/>
</a>
<hr />
<div className="panel">
<div className="header">
<h3>
<strong>
{user?.firstName} {user?.lastName}
</strong>
</h3>
<h4 className="username">
<i className="fa fa-user-o" /> {user?.username}
</h4>
</div>
<hr />
<p>
<i className="fa fa-clock-o" data-test="clock-icon-test" />{' '}
{t('joined')} {moment(user?.createdOn, 'YYYYMMDD').fromNow()}
</p>
<p className="email">
<i className="fa fa-envelope-o" /> {user?.email}
</p>
<p className="roles">
<i className="fa fa-lock" />{' '}
{Object.keys(user?.roles || {}).join(', ')}
</p>
<p>
<i className="fa fa-key" />
&nbsp;
<span className="text-muted">{t('id')}:</span>&nbsp;
<span className="user-id">{user?.userId}</span>
</p>
</div>
</StyledContainer>
);
}

View File

@ -1,48 +0,0 @@
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const user: UserWithPermissionsAndRoles = {
username: 'alpha',
roles: {
Alpha: [
['can_this_form_post', 'ResetMyPasswordView'],
['can_this_form_get', 'ResetMyPasswordView'],
['can_this_form_post', 'UserInfoEditView'],
['can_this_form_get', 'UserInfoEditView'],
],
sql_lab: [
['menu_access', 'SQL Lab'],
['can_search_queries', 'Superset'],
['can_csv', 'Superset'],
],
},
firstName: 'alpha',
lastName: 'alpha',
createdOn: '2016-11-11T12:34:17',
userId: 5,
email: 'alpha@alpha.com',
isActive: true,
isAnonymous: false,
permissions: {
datasource_access: ['table1', 'table2'],
database_access: ['db1', 'db2', 'db3'],
},
};
export const userNoPerms = { ...user, permissions: {} };

View File

@ -1,46 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export type Slice = {
dttm: number;
id: number;
url: string;
title: string;
creator?: string;
creator_url?: string;
viz_type: string;
};
export type Chart = {
id: number;
slice_name: string;
slice_url: string;
created_by_name?: string;
changed_on_dttm: number;
};
export type Activity = {
action: string;
item_title: string;
item_url: string;
time: number;
};
export type ActivityResult = {
result: Activity[];
};

View File

@ -1,43 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Row, Col } from 'src/components';
import { shallow } from 'enzyme';
import Profile from 'src/pages/Profile';
import { user } from 'src/features/profile/fixtures';
describe('Profile', () => {
const mockedProps = {
user,
};
it('is valid', () => {
expect(React.isValidElement(<Profile {...mockedProps} />)).toBe(true);
});
it('renders 2 Col', () => {
const wrapper = shallow(<Profile {...mockedProps} />);
expect(wrapper.find(Row)).toExist();
expect(wrapper.find(Col)).toHaveLength(2);
});
it('renders 4 Tabs', () => {
const wrapper = shallow(<Profile {...mockedProps} />);
expect(wrapper.find('[tab]')).toHaveLength(4);
});
});

View File

@ -1,93 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { t, styled } from '@superset-ui/core';
import { Row, Col } from 'src/components';
import Tabs from 'src/components/Tabs';
import { BootstrapUser } from 'src/types/bootstrapTypes';
import Favorites from 'src/features/profile/Favorites';
import UserInfo from 'src/features/profile/UserInfo';
import Security from 'src/features/profile/Security';
import RecentActivity from 'src/features/profile/RecentActivity';
import CreatedContent from 'src/features/profile/CreatedContent';
interface AppProps {
user: BootstrapUser;
}
const StyledTabPane = styled(Tabs.TabPane)`
background-color: ${({ theme }) => theme.colors.grayscale.light5};
padding: ${({ theme }) => theme.gridUnit * 4}px;
`;
export default function App({ user }: AppProps) {
return (
<div className="container app">
<Row gutter={16}>
<Col xs={24} md={6}>
<UserInfo user={user} />
</Col>
<Col xs={24} md={18}>
<Tabs centered>
<StyledTabPane
key="1"
tab={
<div>
<i className="fa fa-star" /> {t('Favorites')}
</div>
}
>
<Favorites user={user} />
</StyledTabPane>
<StyledTabPane
key="2"
tab={
<div>
<i className="fa fa-paint-brush" /> {t('Created content')}
</div>
}
>
<CreatedContent user={user} />
</StyledTabPane>
<StyledTabPane
key="3"
tab={
<div>
<i className="fa fa-list" /> {t('Recent activity')}
</div>
}
>
<RecentActivity user={user} />
</StyledTabPane>
<StyledTabPane
key="4"
tab={
<div>
<i className="fa fa-lock" /> {t('Security & Access')}
</div>
}
>
<Security user={user} />
</StyledTabPane>
</Tabs>
</Col>
</Row>
</div>
);
}

View File

@ -109,7 +109,6 @@ export interface NavBarProps {
user_info_url: string;
user_login_url: string;
user_logout_url: string;
user_profile_url: string | null;
locale: string;
}

View File

@ -123,10 +123,6 @@ const RowLevelSecurityList = lazy(
),
);
const Profile = lazy(
() => import(/* webpackChunkName: "Profile" */ 'src/pages/Profile'),
);
type Routes = {
path: string;
Component: React.ComponentType;
@ -225,10 +221,6 @@ export const routes: Routes = [
path: '/rowlevelsecurity/list',
Component: RowLevelSecurityList,
},
{
path: '/profile',
Component: Profile,
},
{
path: '/sqllab/',
Component: SqlLab,

View File

@ -475,9 +475,7 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# otherwise enabling this flag won't have any effect on the DB.
"SSH_TUNNELING": False,
"AVOID_COLORS_COLLISION": True,
# Set to False to only allow viewing own recent activity
# or to disallow users from viewing other users profile page
# Do not show user info or profile in the menu
# Do not show user info in the menu
"MENU_HIDE_USER_INFO": False,
# Allows users to add a ``superset://`` DB that can query across databases. This is
# an experimental feature with potential security and performance risks, so use with

View File

@ -183,7 +183,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
from superset.views.key_value import KV
from superset.views.log.api import LogRestApi
from superset.views.log.views import LogModelView
from superset.views.profile import ProfileView
from superset.views.redirects import R
from superset.views.sql_lab.views import (
SavedQueryView,
@ -311,7 +310,6 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_view_no_menu(ExplorePermalinkView)
appbuilder.add_view_no_menu(KV)
appbuilder.add_view_no_menu(R)
appbuilder.add_view_no_menu(ProfileView)
appbuilder.add_view_no_menu(SavedQueryView)
appbuilder.add_view_no_menu(SavedQueryViewApi)
appbuilder.add_view_no_menu(SliceAsync)

View File

@ -461,11 +461,10 @@ class ImportExportMixin:
return json_to_dict(self.template_params) # type: ignore
def _user_link(user: User) -> Union[Markup, str]:
def _user(user: User) -> str:
if not user:
return ""
url = f"/superset/profile/{user.username}/"
return Markup(f"<a href=\"{url}\">{escape(user) or ''}</a>")
return escape(user)
class AuditMixinNullable(AuditMixin):
@ -512,11 +511,11 @@ class AuditMixinNullable(AuditMixin):
@renders("created_by")
def creator(self) -> Union[Markup, str]:
return _user_link(self.created_by)
return _user(self.created_by)
@property
def changed_by_(self) -> Union[Markup, str]:
return _user_link(self.changed_by)
return _user(self.changed_by)
@renders("changed_on")
def changed_on_(self) -> Markup:

View File

@ -18,7 +18,7 @@
#}
{% set menu = appbuilder.menu %}
{% set app_icon_width = appbuilder.app.config['APP_ICON_WIDTH'] %}
{% set logo_target_path = appbuilder.app.config['LOGO_TARGET_PATH'] or '/profile/{}/'.format(current_user.username) %}
{% set logo_target_path = appbuilder.app.config['LOGO_TARGET_PATH'] or '/' %}
{% set root_path = logo_target_path if not logo_target_path.startswith('/') else '/superset' + logo_target_path if current_user.username is defined else '#' %}
{% block navbar %}

View File

@ -370,9 +370,6 @@ def menu_data(user: User) -> dict[str, Any]:
else appbuilder.get_url_for_userinfo,
"user_logout_url": appbuilder.get_url_for_logout,
"user_login_url": appbuilder.get_url_for_login,
"user_profile_url": None
if user.is_anonymous or is_feature_enabled("MENU_HIDE_USER_INFO")
else "/profile/",
"locale": session.get("locale", "en"),
},
}

View File

@ -968,13 +968,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
),
)
@has_access
@event_logger.log_this
@expose("/profile/")
@deprecated(new_target="/profile")
def profile(self) -> FlaskResponse:
return redirect("/profile/")
@has_access
@event_logger.log_this
@expose(

View File

@ -1,40 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from flask import abort, g
from flask_appbuilder import permission_name
from flask_appbuilder.api import expose
from flask_appbuilder.security.decorators import has_access
from superset import event_logger, security_manager
from superset.superset_typing import FlaskResponse
from .base import BaseSupersetView
class ProfileView(BaseSupersetView):
route_base = "/profile"
class_permission_name = "Profile"
@expose("/")
@has_access
@permission_name("read")
@event_logger.log_this
def root(self) -> FlaskResponse:
user = g.user if hasattr(g, "user") and g.user else None
if not user or security_manager.is_guest_user(user) or user.is_anonymous:
abort(404)
return super().render_app_template()

View File

@ -971,7 +971,6 @@ class TestCore(SupersetTestCase):
urls = [
"/superset/welcome",
f"/superset/dashboard/{dash_id}/",
"/superset/profile/",
f"/explore/?datasource_type=table&datasource_id={tbl_id}",
]
for url in urls:
@ -1203,11 +1202,6 @@ class TestCore(SupersetTestCase):
is True
)
def test_redirect_new_profile(self):
self.login(username="admin")
resp = self.client.get("/superset/profile/")
assert resp.status_code == 302
def test_redirect_new_sqllab(self):
self.login(username="admin")
resp = self.client.get(

View File

@ -1,164 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import prison
import pytest
from superset import db
from superset.connectors.sqla.models import SqlaTable
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices,
load_birth_names_data,
)
from tests.integration_tests.fixtures.public_role import public_role_like_gamma
from tests.integration_tests.insert_chart_mixin import InsertChartMixin
from .base_tests import SupersetTestCase
class TestProfile(InsertChartMixin, SupersetTestCase):
def insert_dashboard_created_by(self, username: str) -> Dashboard:
user = self.get_user(username)
dashboard = self.insert_dashboard(
f"create_title_test",
f"create_slug_test",
[user.id],
created_by=user,
)
return dashboard
@pytest.fixture()
def insert_dashboard_created_by_admin(self):
with self.create_app().app_context():
dashboard = self.insert_dashboard_created_by("admin")
yield dashboard
db.session.delete(dashboard)
db.session.commit()
def insert_chart_created_by(self, username: str) -> Slice:
user = self.get_user(username)
dataset = db.session.query(SqlaTable).first()
chart = self.insert_chart(
f"create_title_test",
[user.id],
dataset.id,
created_by=user,
)
return chart
@pytest.fixture()
def insert_chart_created_by_admin(self):
with self.create_app().app_context():
chart = self.insert_chart_created_by("admin")
yield chart
db.session.delete(chart)
db.session.commit()
@pytest.mark.usefixtures("insert_dashboard_created_by_admin")
@pytest.mark.usefixtures("insert_chart_created_by_admin")
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_user_profile(self, username="admin"):
self.login(username=username)
slc = self.get_slice("Girls", db.session)
dashboard = db.session.query(Dashboard).filter_by(slug="births").first()
# Set a favorite dashboard
self.client.post(f"/api/v1/dashboard/{dashboard.id}/favorites/", json={})
# Set a favorite chart
self.client.post(f"/api/v1/chart/{slc.id}/favorites/", json={})
# Get favorite dashboards:
request_query = {
"columns": ["created_on_delta_humanized", "dashboard_title", "url"],
"filters": [{"col": "id", "opr": "dashboard_is_favorite", "value": True}],
"keys": ["none"],
"order_column": "changed_on",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["count"] == 1
assert resp.json["result"][0]["dashboard_title"] == "USA Births Names"
# Get Favorite Charts
request_query = {
"filters": [{"col": "id", "opr": "chart_is_favorite", "value": True}],
"order_column": "slice_name",
"order_direction": "asc",
"page": 0,
"page_size": 25,
}
url = f"api/v1/chart/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["count"] == 1
assert resp.json["result"][0]["id"] == slc.id
# Get recent activity
url = "/api/v1/log/recent_activity/?q=(page_size:50)"
resp = self.client.get(url)
# TODO data for recent activity varies for sqlite, we should be able to assert
# the returned data
assert resp.status_code == 200
# Get dashboards created by the user
request_query = {
"columns": ["created_on_delta_humanized", "dashboard_title", "url"],
"filters": [
{"col": "created_by", "opr": "dashboard_created_by_me", "value": "me"}
],
"keys": ["none"],
"order_column": "changed_on",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["result"][0]["dashboard_title"] == "create_title_test"
# Get charts created by the user
request_query = {
"columns": ["created_on_delta_humanized", "slice_name", "url"],
"filters": [
{"col": "created_by", "opr": "chart_created_by_me", "value": "me"}
],
"keys": ["none"],
"order_column": "changed_on_delta_humanized",
"order_direction": "desc",
"page": 0,
"page_size": 100,
}
url = f"/api/v1/chart/?q={prison.dumps(request_query)}"
resp = self.client.get(url)
assert resp.json["count"] == 1
assert resp.json["result"][0]["slice_name"] == "create_title_test"
resp = self.get_resp(f"/profile/")
self.assertIn('"app"', resp)
def test_user_profile_gamma(self):
self.login(username="gamma")
resp = self.get_resp(f"/profile/")
self.assertIn('"app"', resp)
@pytest.mark.usefixtures("public_role_like_gamma")
def test_user_profile_anonymous(self):
self.logout()
resp = self.client.get("/profile/")
assert resp.status_code == 404