diff --git a/.betterer.results b/.betterer.results index e1996888277..74c1c477bbd 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3940,6 +3940,35 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], + "public/app/features/provisioning/ConfigForm.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] + ], + "public/app/features/provisioning/DeleteRepositoryButton.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + ], + "public/app/features/provisioning/EditRepositoryPage.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], + "public/app/features/provisioning/RepositoryListPage.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + ], + "public/app/features/provisioning/api/index.ts:5381": [ + [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"] + ], "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/conf/provisioning/sample/classic-dashboard.json b/conf/provisioning/sample/classic-dashboard.json index 1d9818ac50d..d0746c0b20b 100644 --- a/conf/provisioning/sample/classic-dashboard.json +++ b/conf/provisioning/sample/classic-dashboard.json @@ -177,12 +177,8 @@ { "current": { "selected": false, - "text": [ - "All" - ], - "value": [ - "$__all" - ] + "text": ["All"], + "value": ["$__all"] }, "datasource": { "type": "testdata", @@ -210,12 +206,8 @@ { "current": { "selected": false, - "text": [ - "All" - ], - "value": [ - "$__all" - ] + "text": ["All"], + "value": ["$__all"] }, "datasource": { "type": "testdata", @@ -243,14 +235,8 @@ { "current": { "selected": false, - "text": [ - "AAA", - "ACB" - ], - "value": [ - "AAA", - "ACB" - ] + "text": ["AAA", "ACB"], + "value": ["AAA", "ACB"] }, "datasource": { "type": "testdata", @@ -301,17 +287,7 @@ "to": "now" }, "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ] + "refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] }, "timezone": "", "title": "Text options", diff --git a/conf/provisioning/sample/dashboard-a.json b/conf/provisioning/sample/dashboard-a.json index e6a04bb3e35..482d093c23d 100644 --- a/conf/provisioning/sample/dashboard-a.json +++ b/conf/provisioning/sample/dashboard-a.json @@ -33,4 +33,4 @@ } ] } -} \ No newline at end of file +} diff --git a/conf/provisioning/sample/sample-playlist.json b/conf/provisioning/sample/sample-playlist.json index 1b72ccdad59..26473e8bfc8 100644 --- a/conf/provisioning/sample/sample-playlist.json +++ b/conf/provisioning/sample/sample-playlist.json @@ -22,4 +22,4 @@ } ] } -} \ No newline at end of file +} diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 66f50147643..aa45c4fc4be 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -70,6 +70,14 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } + // TODO Add feature toggle / permissions check + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ + Text: "Provisioning", + Id: "provisioning", + SubTitle: "Manage resources from remote repositories", + Url: s.cfg.AppSubURL + "/admin/provisioning", + }) + generalNode := &navtree.NavLink{ Text: "General", SubTitle: "Manage default preferences and settings across Grafana", diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 73b73b52d4d..bb310e94c19 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -29,6 +29,7 @@ import templatingReducers from 'app/features/variables/state/keyedVariablesReduc import { alertingApi } from '../../features/alerting/unified/api/alertingApi'; import { userPreferencesAPI } from '../../features/preferences/api'; +import { provisioningAPI } from '../../features/provisioning/api'; import { queryLibraryApi } from '../../features/query-library/api/factory'; import { cleanUpAction } from '../actions/cleanUp'; @@ -61,6 +62,7 @@ const rootReducers = { [cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer, [queryLibraryApi.reducerPath]: queryLibraryApi.reducer, [userPreferencesAPI.reducerPath]: userPreferencesAPI.reducer, + [provisioningAPI.reducerPath]: provisioningAPI.reducer, }; const addedReducers = {}; diff --git a/public/app/features/provisioning/ConfigForm.tsx b/public/app/features/provisioning/ConfigForm.tsx new file mode 100644 index 00000000000..b313f6372aa --- /dev/null +++ b/public/app/features/provisioning/ConfigForm.tsx @@ -0,0 +1,172 @@ +import { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { AppEvents } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; +import { Field, Combobox, SecretInput, Input, Button, Switch, TextLink, ControlledCollapse } from '@grafana/ui'; +import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; +import { FolderPicker } from 'app/core/components/Select/FolderPicker'; + +import { RepositoryResource } from './api/types'; +import { useCreateOrUpdateRepository } from './hooks'; +import { RepositoryFormData } from './types'; +import { dataToSpec, specToData } from './utils/data'; + +const typeOptions = ['GitHub', 'Local', 'S3'].map((label) => ({ label, value: label.toLowerCase() })); +const appEvents = getAppEvents(); + +export interface ConfigFormProps { + data?: RepositoryResource; +} +export function ConfigForm({ data }: ConfigFormProps) { + const [submitData, request] = useCreateOrUpdateRepository(data?.metadata.name); + const { + register, + handleSubmit, + reset, + control, + formState: { errors, isDirty }, + setValue, + watch, + getValues, + } = useForm({ defaultValues: data ? specToData(data.spec) : { type: 'github' } }); + const [tokenConfigured, setTokenConfigured] = useState(Boolean(data?.metadata?.name)); + const navigate = useNavigate(); + const watchType = watch('type'); + + useEffect(() => { + if (request.isSuccess) { + const formData = getValues(); + + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: ['Repository settings saved'], + }); + reset(formData); + setTimeout(() => { + navigate('/admin/provisioning'); + }, 300); + } + }, [request.isSuccess, reset, getValues, navigate]); + + const onSubmit = (data: RepositoryFormData) => { + const spec = dataToSpec(data); + submitData(spec); + }; + + return ( +
+ + + { + return ( + onChange(value?.value)} + placeholder={'Select repository type'} + disabled={!!data?.spec} + {...field} + /> + ); + }} + /> + + + + + {watchType === 'github' && ( + <> + +

+ To create a new Access Token, navigate to{' '} + + Personal Access Tokens + {' '} + and create a click "Generate new token." +

+ +

Ensure that your token has the following permissions:

+ + For all repositories: +
public_repo, repo:status, repo_deployment, read:packages, read:user, user:email
+ + For GitHub projects: +
read:org, read:project
+ + An extra setting is required for private repositories: +
repo (Full control of private repositories)
+
+ + { + return ( + { + setValue('token', ''); + setTokenConfigured(false); + }} + /> + ); + }} + /> + + + + + + + + + + + + + + + )} + + {watchType === 'local' && ( + + + + )} + + {watchType === 's3' && ( + <> + + + + + + + + )} + + { + return ; + }} + /> + + + + ); +} diff --git a/public/app/features/provisioning/DeleteRepositoryButton.tsx b/public/app/features/provisioning/DeleteRepositoryButton.tsx new file mode 100644 index 00000000000..281139e7ca2 --- /dev/null +++ b/public/app/features/provisioning/DeleteRepositoryButton.tsx @@ -0,0 +1,47 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { AppEvents } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; +import { Button, ConfirmModal } from '@grafana/ui'; + +import { useDeleteRepositoryMutation } from './api'; + +const appEvents = getAppEvents(); +export function DeleteRepositoryButton({ name }: { name: string }) { + const [deleteRepository, request] = useDeleteRepositoryMutation(); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + if (request.isSuccess) { + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: ['Repository settings deleted'], + }); + } + }, [request.isSuccess]); + + const onConfirm = useCallback(() => { + deleteRepository({ name }); + }, [deleteRepository, name]); + + return ( + <> + + setShowModal(false)} + /> + + ); +} diff --git a/public/app/features/provisioning/EditRepositoryPage.tsx b/public/app/features/provisioning/EditRepositoryPage.tsx new file mode 100644 index 00000000000..f73a5b2e72a --- /dev/null +++ b/public/app/features/provisioning/EditRepositoryPage.tsx @@ -0,0 +1,32 @@ +import { useParams } from 'react-router-dom-v5-compat'; + +import { EmptyState, Text, TextLink } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; + +import { ConfigForm } from './ConfigForm'; +import { useGetRepositoryQuery } from './api'; +import { PROVISIONING_URL } from './constants'; + +export default function EditRepositoryPage() { + const { name = '' } = useParams(); + const query = useGetRepositoryQuery({ name }); + //@ts-expect-error TODO add error types + const notFound = query.isError && query.error?.status === 404; + return ( + + + {notFound ? ( + + Make sure the repository config exists in the configuration file. + Back to repositories + + ) : ( + + )} + + + ); +} diff --git a/public/app/features/provisioning/NewRepositoryPage.tsx b/public/app/features/provisioning/NewRepositoryPage.tsx new file mode 100644 index 00000000000..c5b9907221d --- /dev/null +++ b/public/app/features/provisioning/NewRepositoryPage.tsx @@ -0,0 +1,16 @@ +import { Page } from 'app/core/components/Page/Page'; + +import { ConfigForm } from './ConfigForm'; + +export default function NewRepositoryPage() { + return ( + + + + + + ); +} diff --git a/public/app/features/provisioning/RepositoryListPage.tsx b/public/app/features/provisioning/RepositoryListPage.tsx new file mode 100644 index 00000000000..e7c80c95a3a --- /dev/null +++ b/public/app/features/provisioning/RepositoryListPage.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/css'; +import { useState } from 'react'; + +import { Card, EmptySearchResult, EmptyState, FilterInput, LinkButton, Stack } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; + +import { Resource } from '../apiserver/types'; + +import { DeleteRepositoryButton } from './DeleteRepositoryButton'; +import { RepositorySpec } from './api/types'; +import { NEW_URL, PROVISIONING_URL } from './constants'; +import { useRepositoryList } from './hooks'; + +export default function RepositoryListPage() { + const [items, isLoading] = useRepositoryList(); + return ( + + + + + + ); +} + +function RepositoryListPageContent({ items }: { items?: Array> }) { + const [query, setQuery] = useState(''); + if (!items?.length) { + return ( + + Create repository config + + } + /> + ); + } + + const filteredItems = items.filter((item) => item.metadata.name.includes(query)); + + return ( + + + + + Add repository config + + + + {!!filteredItems.length ? ( + filteredItems.map((item) => { + return ( + + + {item.spec.title} + {item.spec.type} + + + + Edit + + + + + ); + }) + ) : ( + No results matching your query + )} + + + ); +} diff --git a/public/app/features/provisioning/api/baseAPI.ts b/public/app/features/provisioning/api/baseAPI.ts new file mode 100644 index 00000000000..4c06b959126 --- /dev/null +++ b/public/app/features/provisioning/api/baseAPI.ts @@ -0,0 +1,41 @@ +import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; +import { lastValueFrom } from 'rxjs'; + +import { BackendSrvRequest, getBackendSrv, config } from '@grafana/runtime'; + +interface RequestOptions extends BackendSrvRequest { + manageError?: (err: unknown) => { error: unknown }; + showErrorAlert?: boolean; + + // rtk codegen sets this + body?: BackendSrvRequest['data']; +} + +function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { + async function backendSrvBaseQuery(requestOptions: RequestOptions) { + try { + const { data: responseData, ...meta } = await lastValueFrom( + getBackendSrv().fetch({ + ...requestOptions, + url: baseURL + requestOptions.url, + showErrorAlert: requestOptions.showErrorAlert, + data: requestOptions.body, + }) + ); + return { data: responseData, meta }; + } catch (error) { + return requestOptions.manageError ? requestOptions.manageError(error) : { error }; + } + } + + return backendSrvBaseQuery; +} + +export const baseAPI = createApi({ + reducerPath: 'provisioningAPI', + baseQuery: createBackendSrvBaseQuery({ + baseURL: `/apis/provisioning.grafana.app/v0alpha1/namespaces/${config.namespace}`, + }), + endpoints: () => ({}), + tagTypes: ['RepositoryList'], +}); diff --git a/public/app/features/provisioning/api/endpoints.ts b/public/app/features/provisioning/api/endpoints.ts new file mode 100644 index 00000000000..4b1948f46f6 --- /dev/null +++ b/public/app/features/provisioning/api/endpoints.ts @@ -0,0 +1,76 @@ +import { baseAPI as api } from './baseAPI'; +import { + RepositoryList, + RepositoryResource, + RequestArg, + UpdateRequestArg, + HelloWorld, + RepositoryForCreate, +} from './types'; + +const BASE_PATH = '/repositories'; + +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + // Queries + listRepository: build.query({ + query: () => ({ url: BASE_PATH }), + providesTags: ['RepositoryList'], + }), + getRepository: build.query({ + query: ({ name }) => ({ url: `${BASE_PATH}/${name}` }), + }), + connectGetRepositoryHello: build.query({ + query: ({ name, whom }) => ({ + url: `${BASE_PATH}/${name}/hello`, + params: { whom }, + }), + }), + + // Mutations + createRepository: build.mutation({ + query: (resource) => ({ + url: BASE_PATH, + method: 'POST', + body: resource, + }), + invalidatesTags: ['RepositoryList'], + }), + updateRepository: build.mutation({ + query: ({ name, body }) => ({ + url: `${BASE_PATH}/${name}`, + method: 'PUT', + body, + }), + }), + deleteRepository: build.mutation({ + query: ({ name }) => ({ + url: `${BASE_PATH}/${name}`, + method: 'DELETE', + }), + invalidatesTags: ['RepositoryList'], + }), + patchRepository: build.mutation({ + query: ({ name, body }) => ({ + url: `${BASE_PATH}/${name}`, + method: 'PATCH', + headers: { 'Content-Type': 'application/merge-patch+json' }, + body, + }), + invalidatesTags: ['RepositoryList'], + }), + }), + overrideExisting: false, +}); + +export { injectedRtkApi as generatedAPI }; + +export const { + useListRepositoryQuery, + useCreateRepositoryMutation, + useDeleteRepositoryMutation, + useGetRepositoryQuery, + useUpdateRepositoryMutation, + usePatchRepositoryMutation, + useConnectGetRepositoryHelloQuery, +} = injectedRtkApi; diff --git a/public/app/features/provisioning/api/index.ts b/public/app/features/provisioning/api/index.ts new file mode 100644 index 00000000000..f654b45a356 --- /dev/null +++ b/public/app/features/provisioning/api/index.ts @@ -0,0 +1,4 @@ +export * from './endpoints'; +import { generatedAPI } from './endpoints'; + +export const provisioningAPI = generatedAPI.enhanceEndpoints({}); diff --git a/public/app/features/provisioning/api/types.ts b/public/app/features/provisioning/api/types.ts new file mode 100644 index 00000000000..607bf7e796c --- /dev/null +++ b/public/app/features/provisioning/api/types.ts @@ -0,0 +1,54 @@ +import { Resource, ResourceForCreate, ResourceList } from '../../apiserver/types'; + +export type GitHubRepositoryConfig = { + branchWorkflow?: boolean; + generateDashboardPreviews?: boolean; + owner: string; + repository: string; + token?: string; +}; + +export type LocalRepositoryConfig = { + path?: string; +}; + +export type S3RepositoryConfig = { + bucket?: string; + region?: string; +}; + +export type RepositorySpec = { + title?: string; + description?: string; + folder?: string; + github?: GitHubRepositoryConfig; + local?: LocalRepositoryConfig; + s3?: S3RepositoryConfig; + type: 'github' | 'local' | 's3'; +}; + +export type WatchEvent = { + object: unknown; + type: string; +}; + +export interface RequestArg { + /** Repository UID */ + name: string; +} + +export type RepositoryForCreate = ResourceForCreate; +export interface UpdateRequestArg extends RequestArg { + /** RepositorySpec */ + body: ResourceForCreate; +} + +export type RepositoryList = ResourceList; + +export type RepositoryResource = Resource; + +export type HelloWorld = { + apiVersion?: string; + kind?: string; + whom?: string; +}; diff --git a/public/app/features/provisioning/constants.ts b/public/app/features/provisioning/constants.ts new file mode 100644 index 00000000000..5ac108f6c9e --- /dev/null +++ b/public/app/features/provisioning/constants.ts @@ -0,0 +1,2 @@ +export const PROVISIONING_URL = '/admin/provisioning'; +export const NEW_URL = `${PROVISIONING_URL}/new`; diff --git a/public/app/features/provisioning/hooks.ts b/public/app/features/provisioning/hooks.ts new file mode 100644 index 00000000000..1db6f8b931c --- /dev/null +++ b/public/app/features/provisioning/hooks.ts @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; + +import { useCreateRepositoryMutation, useListRepositoryQuery, useUpdateRepositoryMutation } from './api'; +import { RepositoryResource, RepositorySpec } from './api/types'; + +export function useCreateOrUpdateRepository(name?: string) { + const [create, createRequest] = useCreateRepositoryMutation(); + const [update, updateRequest] = useUpdateRepositoryMutation(); + + const updateOrCreate = useCallback( + (data: RepositorySpec) => { + if (name) { + return update({ name, body: { metadata: { name }, spec: data } }); + } + return create({ metadata: { generateName: 'repository' }, spec: { ...data, title: 'test' } }); + }, + [create, name, update] + ); + + return [updateOrCreate, name ? updateRequest : createRequest] as const; +} + +// Sort repositories by resourceVersion to show the last modified +export function useRepositoryList(): [RepositoryResource[] | undefined, boolean] { + const query = useListRepositoryQuery(); + + const sortedItems = query.data?.items?.slice().sort((a, b) => { + return Number(b.metadata.resourceVersion) - Number(a.metadata.resourceVersion); + }); + + return [sortedItems, query.isLoading]; +} diff --git a/public/app/features/provisioning/types.ts b/public/app/features/provisioning/types.ts new file mode 100644 index 00000000000..b0e98813c6f --- /dev/null +++ b/public/app/features/provisioning/types.ts @@ -0,0 +1,10 @@ +import { GitHubRepositoryConfig, LocalRepositoryConfig, S3RepositoryConfig } from './api/types'; + +export type RepositoryFormData = GitHubRepositoryConfig & + S3RepositoryConfig & + LocalRepositoryConfig & { + title: string; + description?: string; + folder?: string; + type: 'github' | 'local' | 's3'; + }; diff --git a/public/app/features/provisioning/utils/data.ts b/public/app/features/provisioning/utils/data.ts new file mode 100644 index 00000000000..55e87038f22 --- /dev/null +++ b/public/app/features/provisioning/utils/data.ts @@ -0,0 +1,43 @@ +import { RepositorySpec } from '../api/types'; +import { RepositoryFormData } from '../types'; + +export const dataToSpec = (data: RepositoryFormData): RepositorySpec => { + const spec: RepositorySpec = { + type: data.type, + folder: data.folder, + }; + switch (data.type) { + case 'github': + spec.github = { + branchWorkflow: data.branchWorkflow, + generateDashboardPreviews: data.generateDashboardPreviews, + owner: data.owner, + repository: data.repository, + token: data.token, + }; + break; + case 'local': + spec.local = { + path: data.path, + }; + break; + case 's3': + spec.s3 = { + bucket: data.bucket, + region: data.region, + }; + break; + } + + return spec; +}; + +export const specToData = (spec: RepositorySpec): Partial => { + return { + type: spec.type, + folder: spec.folder, + ...spec.github, + ...spec.local, + ...spec.s3, + }; +}; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 3543f334cdf..7403dec1ae5 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -533,6 +533,24 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "BookmarksPage"*/ 'app/features/bookmarks/BookmarksPage') ), }, + { + path: '/admin/provisioning', + component: SafeDynamicImport( + () => import(/* webpackChunkName: "RepositoryListPage"*/ 'app/features/provisioning/RepositoryListPage') + ), + }, + { + path: '/admin/provisioning/new', + component: SafeDynamicImport( + () => import(/* webpackChunkName: "NewRepositoryPage"*/ 'app/features/provisioning/NewRepositoryPage') + ), + }, + { + path: '/admin/provisioning/edit/:name', + component: SafeDynamicImport( + () => import(/* webpackChunkName: "EditRepositoryPage"*/ 'app/features/provisioning/EditRepositoryPage') + ), + }, ...getPluginCatalogRoutes(), ...getSupportBundleRoutes(), ...getAlertingRoutes(), diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 6697ec42208..1e91962e6d7 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -6,6 +6,7 @@ import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDa import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi'; import { cloudMigrationAPI } from 'app/features/migrate-to-cloud/api'; import { userPreferencesAPI } from 'app/features/preferences/api'; +import { provisioningAPI } from 'app/features/provisioning/api'; import { StoreState } from 'app/types/store'; import { buildInitialState } from '../core/reducers/navModel'; @@ -41,6 +42,7 @@ export function configureStore(initialState?: Partial) { cloudMigrationAPI.middleware, queryLibraryApi.middleware, userPreferencesAPI.middleware, + provisioningAPI.middleware, ...extraMiddleware ), devTools: process.env.NODE_ENV !== 'production', diff --git a/scripts/generate-rtk-apis.ts b/scripts/generate-rtk-apis.ts index 00ad97e9037..53db412d433 100644 --- a/scripts/generate-rtk-apis.ts +++ b/scripts/generate-rtk-apis.ts @@ -36,6 +36,11 @@ const config: ConfigFile = { apiImport: 'baseAPI', filterEndpoints: ['getUserPreferences', 'updateUserPreferences', 'patchUserPreferences'], }, + '../public/app/features/provisioning/api/endpoints.gen.ts': { + apiFile: '../public/app/features/provisioning/api/baseApi.ts', + schemaFile: '../public/app/features/provisioning/api/spec.json', + apiImport: 'baseAPI', + }, }, };