feat(alerts/reports): add refresh action (#12071)

This commit is contained in:
ʈᵃᵢ 2020-12-17 11:29:46 -08:00 committed by GitHub
parent 895fa19d8d
commit d1dfe82d6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 7 deletions

View File

@ -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();
});
});

View 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;

View File

@ -24,8 +24,9 @@ import { Nav, Navbar } from 'react-bootstrap';
import Button, { OnClickHandler } from 'src/components/Button';
const StyledHeader = styled.header`
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
.navbar {
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
margin-bottom: 0;
}
.navbar-header .navbar-brand {
font-weight: ${({ theme }) => theme.typography.weights.bold};
@ -108,7 +109,6 @@ export interface SubMenuProps {
buttons?: Array<ButtonProps>;
name?: string | ReactNode;
tabs?: MenuChild[];
children?: MenuChild[];
activeChild?: MenuChild['name'];
/* 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>;
@ -174,6 +174,7 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
))}
</Nav>
</Navbar>
{props.children}
</StyledHeader>
);
};

View File

@ -19,7 +19,7 @@
import React, { useState, useMemo, useEffect } from 'react';
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 Button from 'src/components/Button';
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 ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DeleteModal from 'src/components/DeleteModal';
import LastUpdated from 'src/components/LastUpdated';
import {
useListViewResource,
useSingleViewResource,
@ -61,6 +63,13 @@ const deleteAlerts = makeApi<number[], { message: string }>({
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({
addDangerToast,
isReportEnabled = false,
@ -86,6 +95,7 @@ function AlertList({
resourceCount: alertsCount,
resourceCollection: alerts,
bulkSelectEnabled,
lastFetched,
},
hasPerm,
fetchData,
@ -397,7 +407,11 @@ function AlertList({
},
]}
buttons={subMenuButtons}
/>
>
<RefreshContainer>
<LastUpdated updatedAt={lastFetched} update={() => refreshData()} />
</RefreshContainer>
</SubMenu>
<AlertReportModal
alert={currentAlert}
addDangerToast={addDangerToast}

View File

@ -35,6 +35,7 @@ interface ListViewResourceState<D extends object = any> {
permissions: string[];
lastFetchDataConfig: FetchDataConfig | null;
bulkSelectEnabled: boolean;
lastFetched?: string;
}
export function useListViewResource<D extends object = any>(
@ -43,7 +44,7 @@ export function useListViewResource<D extends object = any>(
handleErrorMsg: (errorMsg: string) => void,
infoEnable = true,
defaultCollectionValue: D[] = [],
baseFilters: FilterValue[] = [], // must be memoized
baseFilters?: FilterValue[], // must be memoized
) {
const [state, setState] = useState<ListViewResourceState<D>>({
count: 0,
@ -112,7 +113,7 @@ export function useListViewResource<D extends object = any>(
loading: true,
});
const filterExps = baseFilters
const filterExps = (baseFilters || [])
.concat(filterValues)
.map(({ id: col, operator: opr, value }) => ({
col,
@ -136,6 +137,7 @@ export function useListViewResource<D extends object = any>(
updateState({
collection: json.result,
count: json.count,
lastFetched: new Date().toISOString(),
});
},
createErrorHandler(errMsg =>
@ -152,7 +154,7 @@ export function useListViewResource<D extends object = any>(
updateState({ loading: false });
});
},
[baseFilters.length ? baseFilters : null],
[baseFilters],
);
return {
@ -161,6 +163,7 @@ export function useListViewResource<D extends object = any>(
resourceCount: state.count,
resourceCollection: state.collection,
bulkSelectEnabled: state.bulkSelectEnabled,
lastFetched: state.lastFetched,
},
setResourceCollection: (update: D[]) =>
updateState({