mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
feat(alerts/reports): add refresh action (#12071)
This commit is contained in:
parent
895fa19d8d
commit
d1dfe82d6c
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ReactWrapper } from 'enzyme';
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
import LastUpdated from '.';
|
||||||
|
|
||||||
|
describe('LastUpdated', () => {
|
||||||
|
let wrapper: ReactWrapper;
|
||||||
|
const updatedAt = new Date('Sat Dec 12 2020 00:00:00 GMT-0800');
|
||||||
|
|
||||||
|
it('renders the base component (no refresh)', () => {
|
||||||
|
const wrapper = mount(<LastUpdated updatedAt={updatedAt} />);
|
||||||
|
expect(/^Last Updated .+$/.test(wrapper.text())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a refresh action', () => {
|
||||||
|
const mockAction = jest.fn();
|
||||||
|
wrapper = mount(<LastUpdated updatedAt={updatedAt} update={mockAction} />);
|
||||||
|
const props = wrapper.find('[data-test="refresh"]').props();
|
||||||
|
if (props.onClick) {
|
||||||
|
props.onClick({} as React.MouseEvent);
|
||||||
|
}
|
||||||
|
expect(mockAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
80
superset-frontend/src/components/LastUpdated/index.tsx
Normal file
80
superset-frontend/src/components/LastUpdated/index.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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, { useEffect, useState, FunctionComponent } from 'react';
|
||||||
|
import moment, { Moment, MomentInput } from 'moment';
|
||||||
|
import { t, styled } from '@superset-ui/core';
|
||||||
|
import Icon from 'src/components/Icon';
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL = 60000; // every minute
|
||||||
|
|
||||||
|
interface LastUpdatedProps {
|
||||||
|
updatedAt: MomentInput;
|
||||||
|
update?: React.MouseEventHandler<SVGSVGElement>;
|
||||||
|
}
|
||||||
|
moment.updateLocale('en', {
|
||||||
|
calendar: {
|
||||||
|
lastDay: '[Yesterday at] LTS',
|
||||||
|
sameDay: '[Today at] LTS',
|
||||||
|
nextDay: '[Tomorrow at] LTS',
|
||||||
|
lastWeek: '[last] dddd [at] LTS',
|
||||||
|
nextWeek: 'dddd [at] LTS',
|
||||||
|
sameElse: 'L',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const TextStyles = styled.span`
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Refresh = styled(Icon)`
|
||||||
|
color: ${({ theme }) => theme.colors.primary.base};
|
||||||
|
width: auto;
|
||||||
|
height: ${({ theme }) => theme.gridUnit * 5}px;
|
||||||
|
position: relative;
|
||||||
|
top: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
left: ${({ theme }) => theme.gridUnit}px;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
|
||||||
|
updatedAt,
|
||||||
|
update,
|
||||||
|
}) => {
|
||||||
|
const [timeSince, setTimeSince] = useState<Moment>(moment(updatedAt));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeSince(() => moment(updatedAt));
|
||||||
|
|
||||||
|
// update UI every minute in case day changes
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTimeSince(() => moment(updatedAt));
|
||||||
|
}, REFRESH_INTERVAL);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [updatedAt]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextStyles>
|
||||||
|
{t('Last Updated %s', timeSince.isValid() ? timeSince.calendar() : '--')}
|
||||||
|
{update && <Refresh name="refresh" onClick={update} />}
|
||||||
|
</TextStyles>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LastUpdated;
|
@ -24,8 +24,9 @@ import { Nav, Navbar } from 'react-bootstrap';
|
|||||||
import Button, { OnClickHandler } from 'src/components/Button';
|
import Button, { OnClickHandler } from 'src/components/Button';
|
||||||
|
|
||||||
const StyledHeader = styled.header`
|
const StyledHeader = styled.header`
|
||||||
.navbar {
|
|
||||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
.navbar {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.navbar-header .navbar-brand {
|
.navbar-header .navbar-brand {
|
||||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||||
@ -108,7 +109,6 @@ export interface SubMenuProps {
|
|||||||
buttons?: Array<ButtonProps>;
|
buttons?: Array<ButtonProps>;
|
||||||
name?: string | ReactNode;
|
name?: string | ReactNode;
|
||||||
tabs?: MenuChild[];
|
tabs?: MenuChild[];
|
||||||
children?: MenuChild[];
|
|
||||||
activeChild?: MenuChild['name'];
|
activeChild?: MenuChild['name'];
|
||||||
/* If usesRouter is true, a react-router <Link> component will be used instead of href.
|
/* If usesRouter is true, a react-router <Link> component will be used instead of href.
|
||||||
* ONLY set usesRouter to true if SubMenu is wrapped in a react-router <Router>;
|
* ONLY set usesRouter to true if SubMenu is wrapped in a react-router <Router>;
|
||||||
@ -174,6 +174,7 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
|
|||||||
))}
|
))}
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
{props.children}
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { t, SupersetClient, makeApi } from '@superset-ui/core';
|
import { t, SupersetClient, makeApi, styled } from '@superset-ui/core';
|
||||||
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import FacePile from 'src/components/FacePile';
|
import FacePile from 'src/components/FacePile';
|
||||||
@ -37,6 +37,8 @@ import AlertStatusIcon from 'src/views/CRUD/alert/components/AlertStatusIcon';
|
|||||||
import RecipientIcon from 'src/views/CRUD/alert/components/RecipientIcon';
|
import RecipientIcon from 'src/views/CRUD/alert/components/RecipientIcon';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import DeleteModal from 'src/components/DeleteModal';
|
import DeleteModal from 'src/components/DeleteModal';
|
||||||
|
import LastUpdated from 'src/components/LastUpdated';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useListViewResource,
|
useListViewResource,
|
||||||
useSingleViewResource,
|
useSingleViewResource,
|
||||||
@ -61,6 +63,13 @@ const deleteAlerts = makeApi<number[], { message: string }>({
|
|||||||
endpoint: '/api/v1/report/',
|
endpoint: '/api/v1/report/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const RefreshContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 ${({ theme }) => theme.gridUnit * 4}px
|
||||||
|
${({ theme }) => theme.gridUnit * 3}px;
|
||||||
|
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||||
|
`;
|
||||||
|
|
||||||
function AlertList({
|
function AlertList({
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
isReportEnabled = false,
|
isReportEnabled = false,
|
||||||
@ -86,6 +95,7 @@ function AlertList({
|
|||||||
resourceCount: alertsCount,
|
resourceCount: alertsCount,
|
||||||
resourceCollection: alerts,
|
resourceCollection: alerts,
|
||||||
bulkSelectEnabled,
|
bulkSelectEnabled,
|
||||||
|
lastFetched,
|
||||||
},
|
},
|
||||||
hasPerm,
|
hasPerm,
|
||||||
fetchData,
|
fetchData,
|
||||||
@ -397,7 +407,11 @@ function AlertList({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
buttons={subMenuButtons}
|
buttons={subMenuButtons}
|
||||||
/>
|
>
|
||||||
|
<RefreshContainer>
|
||||||
|
<LastUpdated updatedAt={lastFetched} update={() => refreshData()} />
|
||||||
|
</RefreshContainer>
|
||||||
|
</SubMenu>
|
||||||
<AlertReportModal
|
<AlertReportModal
|
||||||
alert={currentAlert}
|
alert={currentAlert}
|
||||||
addDangerToast={addDangerToast}
|
addDangerToast={addDangerToast}
|
||||||
|
@ -35,6 +35,7 @@ interface ListViewResourceState<D extends object = any> {
|
|||||||
permissions: string[];
|
permissions: string[];
|
||||||
lastFetchDataConfig: FetchDataConfig | null;
|
lastFetchDataConfig: FetchDataConfig | null;
|
||||||
bulkSelectEnabled: boolean;
|
bulkSelectEnabled: boolean;
|
||||||
|
lastFetched?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useListViewResource<D extends object = any>(
|
export function useListViewResource<D extends object = any>(
|
||||||
@ -43,7 +44,7 @@ export function useListViewResource<D extends object = any>(
|
|||||||
handleErrorMsg: (errorMsg: string) => void,
|
handleErrorMsg: (errorMsg: string) => void,
|
||||||
infoEnable = true,
|
infoEnable = true,
|
||||||
defaultCollectionValue: D[] = [],
|
defaultCollectionValue: D[] = [],
|
||||||
baseFilters: FilterValue[] = [], // must be memoized
|
baseFilters?: FilterValue[], // must be memoized
|
||||||
) {
|
) {
|
||||||
const [state, setState] = useState<ListViewResourceState<D>>({
|
const [state, setState] = useState<ListViewResourceState<D>>({
|
||||||
count: 0,
|
count: 0,
|
||||||
@ -112,7 +113,7 @@ export function useListViewResource<D extends object = any>(
|
|||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterExps = baseFilters
|
const filterExps = (baseFilters || [])
|
||||||
.concat(filterValues)
|
.concat(filterValues)
|
||||||
.map(({ id: col, operator: opr, value }) => ({
|
.map(({ id: col, operator: opr, value }) => ({
|
||||||
col,
|
col,
|
||||||
@ -136,6 +137,7 @@ export function useListViewResource<D extends object = any>(
|
|||||||
updateState({
|
updateState({
|
||||||
collection: json.result,
|
collection: json.result,
|
||||||
count: json.count,
|
count: json.count,
|
||||||
|
lastFetched: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createErrorHandler(errMsg =>
|
createErrorHandler(errMsg =>
|
||||||
@ -152,7 +154,7 @@ export function useListViewResource<D extends object = any>(
|
|||||||
updateState({ loading: false });
|
updateState({ loading: false });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[baseFilters.length ? baseFilters : null],
|
[baseFilters],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -161,6 +163,7 @@ export function useListViewResource<D extends object = any>(
|
|||||||
resourceCount: state.count,
|
resourceCount: state.count,
|
||||||
resourceCollection: state.collection,
|
resourceCollection: state.collection,
|
||||||
bulkSelectEnabled: state.bulkSelectEnabled,
|
bulkSelectEnabled: state.bulkSelectEnabled,
|
||||||
|
lastFetched: state.lastFetched,
|
||||||
},
|
},
|
||||||
setResourceCollection: (update: D[]) =>
|
setResourceCollection: (update: D[]) =>
|
||||||
updateState({
|
updateState({
|
||||||
|
Loading…
Reference in New Issue
Block a user