mirror of
https://github.com/apache/superset.git
synced 2026-06-04 07:09:22 +00:00
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
/**
|
|
* 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 { makeApi } from '@superset-ui/core';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
export enum ResourceStatus {
|
|
LOADING = 'loading',
|
|
COMPLETE = 'complete',
|
|
ERROR = 'error',
|
|
}
|
|
|
|
/**
|
|
* An object containing the data fetched from the API,
|
|
* as well as loading and error info
|
|
*/
|
|
export type Resource<T> = LoadingState | CompleteState<T> | ErrorState;
|
|
|
|
// Trying out something a little different: a separate type per status.
|
|
// This should let Typescript know whether a Resource has a result or error.
|
|
// It's possible that I'm expecting too much from Typescript here.
|
|
// If this ends up causing problems, we can change the type to:
|
|
//
|
|
// export type Resource<T> = {
|
|
// status: ResourceStatus;
|
|
// result: null | T;
|
|
// error: null | Error;
|
|
// }
|
|
|
|
type LoadingState = {
|
|
status: ResourceStatus.LOADING;
|
|
result: null;
|
|
error: null;
|
|
};
|
|
|
|
type CompleteState<T> = {
|
|
status: ResourceStatus.COMPLETE;
|
|
result: T;
|
|
error: null;
|
|
};
|
|
|
|
type ErrorState = {
|
|
status: ResourceStatus.ERROR;
|
|
result: null;
|
|
error: Error;
|
|
};
|
|
|
|
const initialState: LoadingState = {
|
|
status: ResourceStatus.LOADING,
|
|
result: null,
|
|
error: null,
|
|
};
|
|
|
|
/**
|
|
* A general-purpose hook to fetch the response from an endpoint.
|
|
* Returns the full response body from the API, including metadata.
|
|
*
|
|
* Note: You likely want {useApiV1Resource} instead of this!
|
|
*
|
|
* TODO Store the state in redux or something, share state between hook instances.
|
|
*
|
|
* TODO Include a function in the returned resource object to refresh the data.
|
|
*
|
|
* A core design decision here is composition > configuration,
|
|
* and every hook should only have one job.
|
|
* Please address new needs with new hooks if possible,
|
|
* rather than adding config options to this hook.
|
|
*
|
|
* @param endpoint The url where the resource is located.
|
|
*/
|
|
export function useApiResourceFullBody<RESULT>(
|
|
endpoint: string,
|
|
): Resource<RESULT> {
|
|
const [resource, setResource] = useState<Resource<RESULT>>(initialState);
|
|
const cancelRef = useRef<() => void>(() => {});
|
|
|
|
useEffect(() => {
|
|
// If refresh is implemented, this will need to change.
|
|
// The previous values should stay during refresh.
|
|
setResource(initialState);
|
|
|
|
// when this effect runs, the endpoint has changed.
|
|
// cancel any current calls so that state doesn't get messed up.
|
|
cancelRef.current();
|
|
let cancelled = false;
|
|
cancelRef.current = () => {
|
|
cancelled = true;
|
|
};
|
|
|
|
const fetchResource = makeApi<{}, RESULT>({
|
|
method: 'GET',
|
|
endpoint,
|
|
});
|
|
|
|
fetchResource({})
|
|
.then(result => {
|
|
if (!cancelled) {
|
|
setResource({
|
|
status: ResourceStatus.COMPLETE,
|
|
result,
|
|
error: null,
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (!cancelled) {
|
|
setResource({
|
|
status: ResourceStatus.ERROR,
|
|
result: null,
|
|
error,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Cancel the request when the component un-mounts
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [endpoint]);
|
|
|
|
return resource;
|
|
}
|
|
|
|
/**
|
|
* For when you want to transform the result of an api resource hook before using it.
|
|
*
|
|
* @param resource the Resource object returned from useApiV1Resource
|
|
* @param transformFn a callback that transforms the result object into the shape you want.
|
|
* Make sure to use a persistent function for this so it doesn't constantly recalculate!
|
|
*/
|
|
export function useTransformedResource<IN, OUT>(
|
|
resource: Resource<IN>,
|
|
transformFn: (result: IN) => OUT,
|
|
): Resource<OUT> {
|
|
return useMemo(() => {
|
|
if (resource.status !== ResourceStatus.COMPLETE) {
|
|
// While incomplete, there is no result - no need to transform.
|
|
return resource;
|
|
}
|
|
try {
|
|
return {
|
|
...resource,
|
|
result: transformFn(resource.result),
|
|
};
|
|
} catch (e) {
|
|
return {
|
|
status: ResourceStatus.ERROR,
|
|
result: null,
|
|
error: e,
|
|
};
|
|
}
|
|
}, [resource, transformFn]);
|
|
}
|
|
|
|
// returns the "result" field from a fetched API v1 endpoint
|
|
const extractInnerResult = <T>(responseBody: { result: T }) =>
|
|
responseBody.result;
|
|
|
|
/**
|
|
* A general-purpose hook to fetch a Superset resource from a v1 API endpoint.
|
|
* Handles request lifecycle and async logic so you don't have to.
|
|
*
|
|
* This returns the data under the "result" field in the API response body.
|
|
* If you need the full response body, use {useFullApiResource} instead.
|
|
*
|
|
* @param endpoint The url where the resource is located.
|
|
*/
|
|
export function useApiV1Resource<RESULT>(endpoint: string): Resource<RESULT> {
|
|
return useTransformedResource(
|
|
useApiResourceFullBody<{ result: RESULT }>(endpoint),
|
|
extractInnerResult,
|
|
);
|
|
}
|