Files
teleport/web/packages/shared/hooks/useAsync.ts
T
Rafał Cieślak 6578af1efd Connect: Remove resource cache (#21964)
* Connect: Remove resource cache

This commit refactors parts of the Electron app which still depend on
the resource cache, that is cluster sync and `tsh ssh` executed from the
command bar.

A number of adjustments has been made to executing `tsh ssh` from the
command bar, namely the logic of resolving the hostname has been moved
from the command launcher to useDocumentTerminal. The UI was also
adjusted to allow the user to reconnect to an SSH server if resolving
the hostname fails. This has a nice benefit of also allowing them to
retry if the PTY couldn't be created for some reason.

---

Before gravitational/webapps#1307, Connect was pulling all resources
from the cluster on app start and cluster login. Then it'd show them to
you in tables if you opened a new tab with cluster resources. There were
at least two problems with this approach:

1. It was slow for big clusters as pagination, filtering and sorting
   were all done client-side and blocked the main thread.
2. It wasn't possible to implement advanced search unless we copied the
   implementation from the backend to tshd.

gravitational/webapps#1307 made it so that instead of depending on that
resource cache to show resource tables, Connect would pull paginated
resources from the backend.

However, the resource tables were not the only part of the app depending
on that cache. Due to time constraints, we weren't able to refactor all
of them. We managed to refactor autocomplete (gravitational/webapps#1406)
and the remaining places are listed in #21800.

This PR refactors those remaining places to not use the cache by either
not depending on resource details or by making a call to the proxy
instead.

* Add missing license
2023-02-27 15:32:19 +00:00

191 lines
4.9 KiB
TypeScript

/*
Copyright 2019 Gravitational, Inc.
Licensed 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 { useCallback, useState, useRef, useEffect } from 'react';
/**
* `useAsync` lets you represent the state of an async operation as data. It accepts an async function
* that you want to execute. Calling the hook returns an array of three elements:
*
* * The first element is the representation of the attempt to run that async function as data, the
* so called attempt object.
* * The second element is a function which when called starts to execute the async function.
* * The third element is a function that lets you directly update the attempt object if needed.
*
*
* @example
* export function useUserProfile(userId) {
* const [fetchUserProfileAttempt, fetchUserProfile] = useAsync(async () => {
* return await fetch(`/users/${userId}`);
* })
*
* return { fetchUserProfileAttempt, fetchUserProfile };
* }
*
*
* @example In the view layer you can use it like this:
* function UserProfile(props) {
* const { fetchUserProfileAttempt, fetchUserProfile } = useUserProfile(props.id);
*
* useEffect(() => {
* if (!fetchUserProfileAttempt.status) {
* fetchUserProfile()
* }
* }, [fetchUserProfileAttempt])
*
* switch (fetchUserProfileAttempt.status) {
* case '':
* case 'processing':
* return <Spinner />;
* case 'error':
* return <ErrorMessage text={fetchUserProfileAttempt.statusText} />;
* case 'success':
* return <UserAvatar url={fetchUserProfileAttempt.data.avatarUrl} />;
* }
* }
*
*/
export function useAsync<Args extends unknown[], AttemptData>(
cb: (...args: Args) => Promise<AttemptData>
) {
const [state, setState] = useState<Attempt<AttemptData>>(makeEmptyAttempt);
const isMounted = useIsMounted();
const asyncTask = useRef<Promise<AttemptData>>();
const run = useCallback(
(...args: Args) => {
setState(prevState => ({
...prevState,
status: 'processing',
}));
const promise = cb(...args);
asyncTask.current = promise;
return promise.then(
data => {
if (!isMounted()) {
return [null, new CanceledError()] as [AttemptData, Error];
}
if (asyncTask.current !== promise) {
return [null, new CanceledError()] as [AttemptData, Error];
}
setState(prevState => ({
...prevState,
status: 'success',
data,
}));
return [data, null] as [AttemptData, Error];
},
err => {
if (!isMounted()) {
return [null, new CanceledError()] as [AttemptData, Error];
}
if (asyncTask.current !== promise) {
return [null, new CanceledError()] as [AttemptData, Error];
}
setState(prevState => ({
...prevState,
status: 'error',
statusText: err?.message,
data: null,
}));
return [null, err] as [AttemptData, Error];
}
);
},
[setState, cb, isMounted]
);
const setAttempt = useCallback(
(attempt: Attempt<AttemptData>) => {
setState(attempt);
},
[setState]
);
return [state, run, setAttempt] as const;
}
function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return useCallback(() => isMounted.current, []);
}
export class CanceledError extends Error {
constructor() {
super('Ignored response from stale useAsync request');
this.name = 'CanceledError';
}
}
export type AttemptStatus = 'processing' | 'success' | 'error' | '';
export type Attempt<T> = {
data?: T;
status: AttemptStatus;
statusText: string;
};
export function hasFinished<T>(attempt: Attempt<T>): boolean {
return attempt.status === 'success' || attempt.status === 'error';
}
export function makeEmptyAttempt<T>(): Attempt<T> {
return {
data: null,
status: '',
statusText: '',
};
}
export function makeSuccessAttempt<T>(data: T): Attempt<T> {
return {
data,
status: 'success',
statusText: '',
};
}
export function makeProcessingAttempt<T>(): Attempt<T> {
return {
data: null,
status: 'processing',
statusText: '',
};
}
export function makeErrorAttempt<T>(statusText: string): Attempt<T> {
return {
data: null,
status: 'error',
statusText,
};
}