Skip to content

Commit

Permalink
feat(alerts/reports): add refresh action (#12071)
Browse files Browse the repository at this point in the history
  • Loading branch information
nytai committed Dec 17, 2020
1 parent 895fa19 commit d1dfe82
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 7 deletions.
43 changes: 43 additions & 0 deletions superset-frontend/src/components/LastUpdated/LastUpdated.test.tsx
@@ -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 changes: 80 additions & 0 deletions superset-frontend/src/components/LastUpdated/index.tsx
@@ -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;
5 changes: 3 additions & 2 deletions superset-frontend/src/components/Menu/SubMenu.tsx
Expand Up @@ -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};
Expand Down Expand Up @@ -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>;
Expand Down Expand Up @@ -174,6 +174,7 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
))}
</Nav>
</Navbar>
{props.children}
</StyledHeader>
);
};
Expand Down
18 changes: 16 additions & 2 deletions superset-frontend/src/views/CRUD/alert/AlertList.tsx
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -86,6 +95,7 @@ function AlertList({
resourceCount: alertsCount,
resourceCollection: alerts,
bulkSelectEnabled,
lastFetched,
},
hasPerm,
fetchData,
Expand Down Expand Up @@ -397,7 +407,11 @@ function AlertList({
},
]}
buttons={subMenuButtons}
/>
>
<RefreshContainer>
<LastUpdated updatedAt={lastFetched} update={() => refreshData()} />
</RefreshContainer>
</SubMenu>
<AlertReportModal
alert={currentAlert}
addDangerToast={addDangerToast}
Expand Down
9 changes: 6 additions & 3 deletions superset-frontend/src/views/CRUD/hooks.ts
Expand Up @@ -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>(
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -136,6 +137,7 @@ export function useListViewResource<D extends object = any>(
updateState({
collection: json.result,
count: json.count,
lastFetched: new Date().toISOString(),
});
},
createErrorHandler(errMsg =>
Expand All @@ -152,7 +154,7 @@ export function useListViewResource<D extends object = any>(
updateState({ loading: false });
});
},
[baseFilters.length ? baseFilters : null],
[baseFilters],
);

return {
Expand All @@ -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({
Expand Down

0 comments on commit d1dfe82

Please sign in to comment.