Skip to content

Commit

Permalink
[Asset graph] Add query refresh button with "Date at most X seconds o…
Browse files Browse the repository at this point in the history
…ld" tooltip (#16886)

## Summary & Motivation

Adding back the ability to refresh data and also including a message
indicating how old the last visible refreshed data on the screen is.


## How I Tested These Changes
<img width="294" alt="Screenshot 2023-09-29 at 1 21 20 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/c0e7bfa4-e3d7-4d4c-8344-fbf2a1e87285">
  • Loading branch information
salazarm authored Sep 29, 2023
1 parent 9ec0f16 commit 202cb1d
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const RefreshableCountdown = (props: Props) => {
);
};

const RefreshButton = styled.button`
export const RefreshButton = styled.button`
border: none;
cursor: pointer;
padding: 8px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {Box, Button, Colors, Icon, Tooltip} from '@dagster-io/ui-components';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
import React from 'react';

dayjs.extend(relativeTime);
dayjs.extend(updateLocale);

dayjs.updateLocale('en', {
relativeTime: {
future: 'in %s',
past: '%s ago',
s: '%d seconds',
m: 'a minute',
mm: '%d minutes',
h: 'an hour',
hh: '%d hours',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years',
},
});

export const AssetDataRefreshButton = ({
isRefreshing,
onRefresh,
oldestDataTimestamp,
}: {
isRefreshing: boolean;
onRefresh: () => void;
oldestDataTimestamp: number;
}) => {
return (
<Tooltip
content={
isRefreshing ? (
'Refreshing asset data…'
) : (
<Box flex={{direction: 'column', gap: 4}}>
<TimeFromNowWithSeconds timestamp={oldestDataTimestamp} />
<div>Click to refresh now</div>
</Box>
)
}
>
<Button
icon={<Icon name="refresh" color={Colors.Gray400} />}
onClick={() => {
if (!isRefreshing) {
onRefresh();
}
}}
/>
</Tooltip>
);
};

const TimeFromNowWithSeconds: React.FC<{timestamp: number}> = ({timestamp}) => {
const [text, setText] = React.useState(dayjs(timestamp).fromNow(true));
React.useEffect(() => {
const interval = setInterval(() => {
setText(dayjs(timestamp).fromNow(true));
}, 1000);
return () => {
clearInterval(interval);
};
}, [timestamp]);
return <>{text === '0s' ? 'Refreshing asset data…' : `Data is at most ${text} old`}</>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {AssetKeyInput} from '../graphql/types';
import {isDocumentVisible, useDocumentVisibility} from '../hooks/useDocumentVisibility';
import {useDidLaunchEvent} from '../runs/RunUtils';

import {AssetDataRefreshButton} from './AssetDataRefreshButton';

const _assetKeyListeners: Record<string, Array<DataForNodeListener>> = {};
let providerListener = (_key: string, _data?: LiveDataForNode) => {};
const _cache: Record<string, LiveDataForNode> = {};
Expand Down Expand Up @@ -56,11 +58,13 @@ export function useAssetsLiveData(assetKeys: AssetKeyInput[]) {

return {
liveDataByNode: data,

refresh: React.useCallback(() => {
_resetLastFetchedOrRequested(assetKeys);
setNeedsImmediateFetch();
setIsRefreshing(true);
}, [setNeedsImmediateFetch, assetKeys]),

refreshing: React.useMemo(() => {
for (const key of assetKeys) {
const stringKey = tokenForAssetKey(key);
Expand Down Expand Up @@ -120,6 +124,16 @@ const AssetLiveDataContext = React.createContext<{
onUnsubscribed: () => {},
});

const AssetLiveDataRefreshContext = React.createContext<{
isGloballyRefreshing: boolean;
oldestDataTimestamp: number;
refresh: () => void;
}>({
isGloballyRefreshing: false,
oldestDataTimestamp: Infinity,
refresh: () => {},
});

// Map of asset keys to their last fetched time and last requested time
const lastFetchedOrRequested: Record<
string,
Expand All @@ -143,33 +157,55 @@ export const AssetLiveDataProvider = ({children}: {children: React.ReactNode}) =

const isDocumentVisible = useDocumentVisibility();

const [isGloballyRefreshing, setIsGloballyRefreshing] = React.useState(false);
const [oldestDataTimestamp, setOldestDataTimestamp] = React.useState(0);

const onUpdatingOrUpdated = React.useCallback(() => {
const allAssetKeys = Object.keys(_assetKeyListeners).filter(
(key) => _assetKeyListeners[key]?.length,
);
let isRefreshing = allAssetKeys.length ? true : false;
let oldestDataTimestamp = Infinity;
for (const key of allAssetKeys) {
if (lastFetchedOrRequested[key]?.fetched) {
isRefreshing = false;
}
oldestDataTimestamp = Math.min(
oldestDataTimestamp,
lastFetchedOrRequested[key]?.fetched ?? Infinity,
);
}
setIsGloballyRefreshing(isRefreshing);
setOldestDataTimestamp(oldestDataTimestamp === Infinity ? 0 : oldestDataTimestamp);
}, []);

React.useEffect(() => {
if (!isDocumentVisible) {
return;
}
// Check for assets to fetch every 5 seconds to simplify logic
// This means assets will be fetched at most 5 + SUBSCRIPTION_IDLE_POLL_RATE after their first fetch
// but then will be fetched every SUBSCRIPTION_IDLE_POLL_RATE after that
const interval = setInterval(() => fetchData(client), 5000);
fetchData(client);
const interval = setInterval(() => fetchData(client, onUpdatingOrUpdated), 5000);
fetchData(client, onUpdatingOrUpdated);
return () => {
clearInterval(interval);
};
}, [client, isDocumentVisible]);
}, [client, isDocumentVisible, onUpdatingOrUpdated]);

React.useEffect(() => {
if (!needsImmediateFetch) {
return;
}
const timeout = setTimeout(() => {
fetchData(client);
fetchData(client, onUpdatingOrUpdated);
setNeedsImmediateFetch(false);
// Wait BATCHING_INTERVAL before doing fetch in case the component is unmounted quickly (eg. in the case of scrolling/filtering quickly)
}, BATCHING_INTERVAL);
return () => {
clearTimeout(timeout);
};
}, [client, needsImmediateFetch]);
}, [client, needsImmediateFetch, onUpdatingOrUpdated]);

React.useEffect(() => {
providerListener = (stringKey, assetData) => {
Expand Down Expand Up @@ -240,7 +276,19 @@ export const AssetLiveDataProvider = ({children}: {children: React.ReactNode}) =
[],
)}
>
{children}
<AssetLiveDataRefreshContext.Provider
value={{
isGloballyRefreshing,
oldestDataTimestamp,
refresh: React.useCallback(() => {
setIsGloballyRefreshing(true);
_resetLastFetchedOrRequested();
setNeedsImmediateFetch(true);
}, [setNeedsImmediateFetch]),
}}
>
{children}
</AssetLiveDataRefreshContext.Provider>
</AssetLiveDataContext.Provider>
);
};
Expand All @@ -250,6 +298,7 @@ async function _batchedQueryAssets(
assetKeys: AssetKeyInput[],
client: ApolloClient<any>,
setData: (data: Record<string, LiveDataForNode>) => void,
onUpdatingOrUpdated: () => void,
) {
// Bail if the document isn't visible
if (!assetKeys.length || isFetching) {
Expand All @@ -263,6 +312,7 @@ async function _batchedQueryAssets(
requested: requestTime,
};
});
onUpdatingOrUpdated();
const data = await _queryAssetKeys(client, assetKeys);
const fetchedTime = Date.now();
assetKeys.forEach((key) => {
Expand All @@ -271,10 +321,11 @@ async function _batchedQueryAssets(
};
});
setData(data);
onUpdatingOrUpdated();
isFetching = false;
const nextAssets = _determineAssetsToFetch();
if (nextAssets.length) {
_batchedQueryAssets(nextAssets, client, setData);
_batchedQueryAssets(nextAssets, client, setData, onUpdatingOrUpdated);
}
}

Expand Down Expand Up @@ -333,19 +384,24 @@ function _determineAssetsToFetch() {
return assetsWithoutData.concat(assetsToFetch).slice(0, BATCH_SIZE);
}

function fetchData(client: ApolloClient<any>) {
_batchedQueryAssets(_determineAssetsToFetch(), client, (data) => {
Object.entries(data).forEach(([key, assetData]) => {
const listeners = _assetKeyListeners[key];
providerListener(key, assetData);
if (!listeners) {
return;
}
listeners.forEach((listener) => {
listener(key, assetData);
function fetchData(client: ApolloClient<any>, onUpdatingOrUpdated: () => void) {
_batchedQueryAssets(
_determineAssetsToFetch(),
client,
(data) => {
Object.entries(data).forEach(([key, assetData]) => {
const listeners = _assetKeyListeners[key];
providerListener(key, assetData);
if (!listeners) {
return;
}
listeners.forEach((listener) => {
listener(key, assetData);
});
});
});
});
},
onUpdatingOrUpdated,
);
}

function getAllAssetKeysWithListeners(): AssetKeyInput[] {
Expand All @@ -370,3 +426,16 @@ export function _setCacheEntryForTest(assetKey: AssetKeyInput, data?: LiveDataFo
});
}
}

export function AssetLiveDataRefresh() {
const {isGloballyRefreshing, oldestDataTimestamp, refresh} = React.useContext(
AssetLiveDataRefreshContext,
);
return (
<AssetDataRefreshButton
isRefreshing={isGloballyRefreshing}
oldestDataTimestamp={oldestDataTimestamp}
onRefresh={refresh}
/>
);
}
Loading

2 comments on commit 202cb1d

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for dagit-storybook ready!

✅ Preview
https://dagit-storybook-putaxeg2c-elementl.vercel.app

Built with commit 202cb1d.
This pull request is being automatically deployed with vercel-action

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for dagit-core-storybook ready!

✅ Preview
https://dagit-core-storybook-mbuffcdsd-elementl.vercel.app

Built with commit 202cb1d.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.