diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index b4203991570..07e871a703e 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -17,6 +17,7 @@ import panelsReducers from 'app/features/panel/state/reducers'; import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/reducer'; import userReducers from 'app/features/profile/state/reducers'; import serviceAccountsReducer from 'app/features/serviceaccounts/state/reducers'; +import supportBundlesReducer from 'app/features/support-bundles/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; import templatingReducers from 'app/features/variables/state/keyedVariablesReducer'; @@ -43,6 +44,7 @@ const rootReducers = { ...panelEditorReducers, ...panelsReducers, ...templatingReducers, + ...supportBundlesReducer, plugins: pluginsReducer, [alertingApi.reducerPath]: alertingApi.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, diff --git a/public/app/features/support-bundles/SupportBundles.tsx b/public/app/features/support-bundles/SupportBundles.tsx index 0e75a1c27a6..a2a6fa98f72 100644 --- a/public/app/features/support-bundles/SupportBundles.tsx +++ b/public/app/features/support-bundles/SupportBundles.tsx @@ -1,10 +1,13 @@ import React, { useEffect } from 'react'; -import { useAsyncFn } from 'react-use'; +import { connect, ConnectedProps } from 'react-redux'; import { dateTimeFormat } from '@grafana/data'; -import { config, getBackendSrv } from '@grafana/runtime'; -import { LinkButton } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types'; + +import { loadBundles, removeBundle, checkBundles } from './state/actions'; const subTitle = ( @@ -13,39 +16,48 @@ const subTitle = ( ); -const newButton = ( +const NewBundleButton = ( New support bundle ); -type SupportBundleState = 'complete' | 'error' | 'timeout' | 'pending'; - -interface SupportBundle { - uid: string; - state: SupportBundleState; - creator: string; - createdAt: number; - expiresAt: number; -} - -const getBundles = () => { - return getBackendSrv().get('/api/support-bundles'); +const mapStateToProps = (state: StoreState) => { + return { + supportBundles: state.supportBundles.supportBundles, + isLoading: state.supportBundles.isLoading, + }; }; -function SupportBundles() { - const [bundlesState, fetchBundles] = useAsyncFn(getBundles, []); +const mapDispatchToProps = { + loadBundles, + removeBundle, + checkBundles, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = ConnectedProps; + +const SupportBundlesUnconnected = ({ supportBundles, isLoading, loadBundles, removeBundle, checkBundles }: Props) => { + const isPending = supportBundles.some((b) => b.state === 'pending'); useEffect(() => { - fetchBundles(); - }, [fetchBundles]); + loadBundles(); + }, [loadBundles]); - const actions = config.featureToggles.topnav ? newButton : undefined; + useEffect(() => { + if (isPending) { + checkBundles(); + } + }); + + const actions = config.featureToggles.topnav ? NewBundleButton : undefined; return ( - - {!config.featureToggles.topnav && newButton} + + {!config.featureToggles.topnav && NewBundleButton} @@ -53,25 +65,31 @@ function SupportBundles() { + - {bundlesState?.value?.map((b) => ( - - - - + {supportBundles?.map((bundle) => ( + + + + + + ))} @@ -79,6 +97,6 @@ function SupportBundles() { ); -} +}; -export default SupportBundles; +export default connector(SupportBundlesUnconnected); diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index 3ecbc3212f6..d8070c1c096 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -1,29 +1,11 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useAsyncFn } from 'react-use'; +import React, { useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; -import { getBackendSrv, locationService } from '@grafana/runtime'; -import { Form, Button, Field, Checkbox } from '@grafana/ui'; +import { Form, Button, Field, Checkbox, LinkButton, HorizontalGroup, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types'; -// move to types -export interface SupportBundleCreateRequest { - collectors: string[]; -} - -export interface SupportBundleCollector { - uid: string; - displayName: string; - description: string; - includedByDefault: boolean; - default: boolean; -} - -export interface Props {} - -const createSupportBundle = async (data: SupportBundleCreateRequest) => { - const result = await getBackendSrv().post('/api/support-bundles', data); - return result; -}; +import { loadSupportBundleCollectors, createSupportBundle } from './state/actions'; const subTitle = ( @@ -31,50 +13,60 @@ const subTitle = ( ); -export const SupportBundlesCreate = ({}: Props): JSX.Element => { - const onSubmit = useCallback(async (data) => { - try { - const selectedLabelsArray = Object.keys(data).filter((key) => data[key]); - const response = await createSupportBundle({ collectors: selectedLabelsArray }); - console.info(response); - } catch (e) { - console.error(e); - } +const mapStateToProps = (state: StoreState) => { + return { + collectors: state.supportBundles.supportBundleCollectors, + isLoading: state.supportBundles.createBundlePageLoading, + loadCollectorsError: state.supportBundles.loadBundlesError, + createBundleError: state.supportBundles.createBundleError, + }; +}; - locationService.push('/admin/support-bundles'); - }, []); +const mapDispatchToProps = { + loadSupportBundleCollectors, + createSupportBundle, +}; - const [components, setComponents] = useState([]); - // populate components from the backend - const populateComponents = async () => { - return await getBackendSrv().get('/api/support-bundles/collectors'); +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = ConnectedProps; + +export const SupportBundlesCreateUnconnected = ({ + collectors, + isLoading, + loadCollectorsError, + createBundleError, + loadSupportBundleCollectors, + createSupportBundle, +}: Props): JSX.Element => { + const onSubmit = (data: Record) => { + const selectedLabelsArray = Object.keys(data).filter((key) => data[key]); + createSupportBundle({ collectors: selectedLabelsArray }); }; - const [state, fetchComponents] = useAsyncFn(populateComponents); useEffect(() => { - fetchComponents().then((res) => { - setComponents(res); - }); - }, [fetchComponents]); + loadSupportBundleCollectors(); + }, [loadSupportBundleCollectors]); // turn components into a uuid -> enabled map - const values: Record = components.reduce((acc, curr) => { + const values: Record = collectors.reduce((acc, curr) => { return { ...acc, [curr.uid]: curr.default }; }, {}); return ( - +

Create support bundle

- {state.error &&

{state.error}

} - {!!components.length && ( + {loadCollectorsError && } + {createBundleError && } + {!!collectors.length && (
{({ register, errors }) => { return ( <> - {components.map((component) => { + {collectors.map((component) => { return ( { ); })} - + + + + Cancel + + ); }} @@ -99,4 +96,4 @@ export const SupportBundlesCreate = ({}: Props): JSX.Element => { ); }; -export default SupportBundlesCreate; +export default connector(SupportBundlesCreateUnconnected); diff --git a/public/app/features/support-bundles/state/actions.ts b/public/app/features/support-bundles/state/actions.ts new file mode 100644 index 00000000000..6deb633c207 --- /dev/null +++ b/public/app/features/support-bundles/state/actions.ts @@ -0,0 +1,72 @@ +import { throttle } from 'lodash'; + +import { getBackendSrv, locationService } from '@grafana/runtime'; +import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest, ThunkResult } from 'app/types'; + +import { + collectorsFetchBegin, + collectorsFetchEnd, + fetchBegin, + fetchEnd, + setCreateBundleError, + setLoadBundleError, + supportBundleCollectorsLoaded, + supportBundlesLoaded, +} from './reducers'; + +export function loadBundles(skipPageRefresh = false): ThunkResult { + return async (dispatch) => { + try { + if (!skipPageRefresh) { + dispatch(fetchBegin()); + } + const result = await getBackendSrv().get('/api/support-bundles'); + dispatch(supportBundlesLoaded(result)); + } finally { + dispatch(fetchEnd()); + } + }; +} + +const checkBundlesStatusThrottled = throttle(async (dispatch) => { + const result = await getBackendSrv().get('/api/support-bundles'); + dispatch(supportBundlesLoaded(result)); +}, 1000); + +export function checkBundles(): ThunkResult { + return async (dispatch) => { + dispatch(checkBundlesStatusThrottled); + }; +} + +export function removeBundle(uid: string): ThunkResult { + return async (dispatch) => { + await getBackendSrv().delete(`/api/support-bundles/${uid}`); + dispatch(loadBundles(true)); + }; +} + +export function loadSupportBundleCollectors(): ThunkResult { + return async (dispatch) => { + try { + dispatch(collectorsFetchBegin()); + const result = await getBackendSrv().get('/api/support-bundles/collectors'); + dispatch(supportBundleCollectorsLoaded(result)); + } catch (err) { + dispatch(setLoadBundleError('Error loading support bundles data collectors')); + } finally { + dispatch(collectorsFetchEnd()); + } + }; +} + +export function createSupportBundle(data: SupportBundleCreateRequest): ThunkResult { + return async (dispatch) => { + try { + await getBackendSrv().post('/api/support-bundles', data); + locationService.push('/admin/support-bundles'); + } catch (err) { + dispatch(setCreateBundleError('Error creating support bundle')); + } + }; +} diff --git a/public/app/features/support-bundles/state/reducers.ts b/public/app/features/support-bundles/state/reducers.ts new file mode 100644 index 00000000000..21e32402ec0 --- /dev/null +++ b/public/app/features/support-bundles/state/reducers.ts @@ -0,0 +1,60 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { SupportBundle, SupportBundleCollector, SupportBundlesState } from 'app/types'; + +export const initialState: SupportBundlesState = { + supportBundles: [], + isLoading: false, + supportBundleCollectors: [], + createBundlePageLoading: false, + loadBundlesError: '', + createBundleError: '', +}; + +const supportBundlesSlice = createSlice({ + name: 'supportBundles', + initialState, + reducers: { + supportBundlesLoaded: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, supportBundles: action.payload, isLoading: false }; + }, + fetchBegin: (state): SupportBundlesState => { + return { ...state, isLoading: true }; + }, + fetchEnd: (state): SupportBundlesState => { + return { ...state, isLoading: false }; + }, + collectorsFetchBegin: (state): SupportBundlesState => { + return { ...state, createBundlePageLoading: true }; + }, + collectorsFetchEnd: (state): SupportBundlesState => { + return { ...state, createBundlePageLoading: false }; + }, + supportBundleCollectorsLoaded: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, supportBundleCollectors: action.payload, createBundlePageLoading: false }; + }, + setLoadBundleError: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, loadBundlesError: action.payload, supportBundleCollectors: [] }; + }, + setCreateBundleError: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, createBundleError: action.payload }; + }, + }, +}); + +export const { + supportBundlesLoaded, + fetchBegin, + fetchEnd, + supportBundleCollectorsLoaded, + collectorsFetchBegin, + collectorsFetchEnd, + setLoadBundleError, + setCreateBundleError, +} = supportBundlesSlice.actions; + +export const supportBundlesReducer = supportBundlesSlice.reducer; + +export default { + supportBundles: supportBundlesReducer, +}; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index f2895ca0785..7db846c12bb 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -575,8 +575,7 @@ export function getSupportBundleRoutes(cfg = config): RouteDescriptor[] { { path: '/admin/support-bundles/create', component: SafeDynamicImport( - () => - import(/* webpackChunkName: "ServiceAccountCreatePage" */ 'app/features/support-bundles/SupportBundlesCreate') + () => import(/* webpackChunkName: "SupportBundlesCreate" */ 'app/features/support-bundles/SupportBundlesCreate') ), }, ]; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 3ffade53998..ec74cb8539e 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -18,6 +18,7 @@ export * from './appEvent'; export * from './query'; export * from './preferences'; export * from './accessControl'; +export * from './supportBundles'; import * as CoreEvents from './events'; export { CoreEvents }; diff --git a/public/app/types/supportBundles.ts b/public/app/types/supportBundles.ts new file mode 100644 index 00000000000..5e895cd17aa --- /dev/null +++ b/public/app/types/supportBundles.ts @@ -0,0 +1,30 @@ +type SupportBundleState = 'complete' | 'error' | 'timeout' | 'pending'; + +export interface SupportBundle { + uid: string; + state: SupportBundleState; + creator: string; + createdAt: number; + expiresAt: number; +} + +export interface SupportBundlesState { + supportBundles: SupportBundle[]; + isLoading: boolean; + createBundlePageLoading: boolean; + supportBundleCollectors: SupportBundleCollector[]; + loadBundlesError: string; + createBundleError: string; +} + +export interface SupportBundleCollector { + uid: string; + displayName: string; + description: string; + includedByDefault: boolean; + default: boolean; +} + +export interface SupportBundleCreateRequest { + collectors: string[]; +}
Created on Requested by Expires +
{dateTimeFormat(b.createdAt * 1000)}{b.creator}{dateTimeFormat(b.expiresAt * 1000)}
{dateTimeFormat(bundle.createdAt * 1000)}{bundle.creator}{dateTimeFormat(bundle.expiresAt * 1000)}{bundle.state === 'pending' && } Download + removeBundle(bundle.uid)} name="trash-alt" variant="destructive" /> +