Provisioning: Add config page (#96550)

* Add baseAPI

* Restore schemaFile

* Generate endpoints

* Cleanup endpoints

* Update endpoints

* Set up config page

* Add hook

* Rename to provisioning

* Register reducer and middleware

* Separate List and New pages

* Add GitHub config

* Save data

* Fix types

* Display list

* Fix redirect

* Update tags

* Common folder

* Cleanup

* Update public/app/routes/routes.tsx

Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>

* Add token permissions instructions

* Add EditPage

* load data for edit

* Update hook

* Show api errors

* Add title and token

* Use test name

* Update token field state

* Add delete button

* Add feedback for delete action

* Sort list

* Do not load items for new page

* Prettier

* Disable type change for edit

* Add title field

* Update pkg/services/navtree/navtreeimpl/admin.go

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>

* Update public/app/features/provisioning/ConfigForm.tsx

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>

---------

Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-11-21 09:36:02 +02:00
1 parent ac25114585
commit cd1998e66a
22 files changed
+677 -33

No files matched your search

+29
View File
@@ -3940,6 +3940,35 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
],
"public/app/features/provisioning/ConfigForm.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
],
"public/app/features/provisioning/DeleteRepositoryButton.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/provisioning/EditRepositoryPage.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
],
"public/app/features/provisioning/RepositoryListPage.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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"],
@@ -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",
+1 -1
View File
@@ -33,4 +33,4 @@
}
]
}
}
}
@@ -22,4 +22,4 @@
}
]
}
}
}
@@ -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",
+2
View File
@@ -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 = {};
@@ -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<RepositoryFormData>({ 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 (
<form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: 700 }}>
<FormPrompt onDiscard={reset} confirmRedirect={isDirty} />
<Field label={'Repository type'}>
<Controller
name={'type'}
control={control}
render={({ field: { ref, onChange, ...field } }) => {
return (
<Combobox
options={typeOptions}
onChange={(value) => onChange(value?.value)}
placeholder={'Select repository type'}
disabled={!!data?.spec}
{...field}
/>
);
}}
/>
</Field>
<Field
label={'Title'}
description={'A human-readable name for the config'}
invalid={!!errors.title}
error={errors?.title?.message}
>
<Input {...register('title', { required: 'This field is required.' })} placeholder={'My config'} />
</Field>
{watchType === 'github' && (
<>
<ControlledCollapse collapsible label="Access Token Permissions" isOpen>
<p>
To create a new Access Token, navigate to{' '}
<TextLink external href="https://github.com/settings/tokens">
Personal Access Tokens
</TextLink>{' '}
and create a click &quot;Generate new token.&quot;
</p>
<p>Ensure that your token has the following permissions:</p>
<b>For all repositories:</b>
<pre>public_repo, repo:status, repo_deployment, read:packages, read:user, user:email</pre>
<b>For GitHub projects:</b>
<pre>read:org, read:project</pre>
<b>An extra setting is required for private repositories:</b>
<pre>repo (Full control of private repositories)</pre>
</ControlledCollapse>
<Field label={'GitHub token'} required error={errors?.token?.message} invalid={!!errors.token}>
<Controller
name={'token'}
control={control}
rules={{ required: 'This field is required.' }}
render={({ field: { ref, ...field } }) => {
return (
<SecretInput
{...field}
id={'token'}
placeholder={'ghp_yourTokenHere1234567890abcdEFGHijklMNOP'}
isConfigured={tokenConfigured}
onReset={() => {
setValue('token', '');
setTokenConfigured(false);
}}
/>
);
}}
/>
</Field>
<Field label={'Repository owner'} error={errors?.owner?.message} invalid={!!errors?.owner}>
<Input {...register('owner', { required: 'This field is required.' })} placeholder={'test'} />
</Field>
<Field label={'Repository name'} error={errors?.repository?.message} invalid={!!errors?.repository}>
<Input {...register('repository', { required: 'This field is required.' })} placeholder={'example'} />
</Field>
<Field label={'Commit directly to main'}>
<Switch {...register('branchWorkflow')} id={'branchWorkflow'} />
</Field>
<Field label={'Show dashboard previews'}>
<Switch {...register('generateDashboardPreviews')} id={'generateDashboardPreviews'} />
</Field>
</>
)}
{watchType === 'local' && (
<Field label={'Local path'} error={errors?.path?.message} invalid={!!errors?.path}>
<Input {...register('path', { required: 'This field is required.' })} placeholder={'/path/to/repo'} />
</Field>
)}
{watchType === 's3' && (
<>
<Field label={'S3 bucket'} error={errors?.bucket?.message} invalid={!!errors?.bucket}>
<Input {...register('bucket', { required: 'This field is required.' })} placeholder={'bucket-name'} />
</Field>
<Field label={'S3 region'} error={errors?.region?.message} invalid={!!errors?.region}>
<Input {...register('region', { required: 'This field is required.' })} placeholder={'us-west-2'} />
</Field>
</>
)}
<Field label={'Target folder'}>
<Controller
control={control}
name={'folder'}
render={({ field: { ref, ...field } }) => {
return <FolderPicker {...field} />;
}}
/>
</Field>
<Button type={'submit'}>Save</Button>
</form>
);
}
@@ -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 (
<>
<Button
variant="destructive"
onClick={() => {
setShowModal(true);
}}
>
Delete
</Button>
<ConfirmModal
isOpen={showModal}
title={'Delete repository config'}
body={'Are you sure you want to delete the repository config?'}
confirmText={'Delete'}
onConfirm={onConfirm}
onDismiss={() => setShowModal(false)}
/>
</>
);
}
@@ -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 (
<Page
navId="provisioning"
pageNav={{ text: 'Configure repository', subTitle: 'Configure a repository for storing your resources.' }}
>
<Page.Contents isLoading={query.isLoading}>
{notFound ? (
<EmptyState message={`Repository config not found`} variant="not-found">
<Text element={'p'}>Make sure the repository config exists in the configuration file.</Text>
<TextLink href={PROVISIONING_URL}>Back to repositories</TextLink>
</EmptyState>
) : (
<ConfigForm data={query.data} />
)}
</Page.Contents>
</Page>
);
}
@@ -0,0 +1,16 @@
import { Page } from 'app/core/components/Page/Page';
import { ConfigForm } from './ConfigForm';
export default function NewRepositoryPage() {
return (
<Page
navId="provisioning"
pageNav={{ text: 'Configure repository', subTitle: 'Configure a repository for storing your resources.' }}
>
<Page.Contents>
<ConfigForm />
</Page.Contents>
</Page>
);
}
@@ -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 (
<Page navId="provisioning" subTitle="View and manage your configured repositories">
<Page.Contents isLoading={isLoading}>
<RepositoryListPageContent items={items} />
</Page.Contents>
</Page>
);
}
function RepositoryListPageContent({ items }: { items?: Array<Resource<RepositorySpec>> }) {
const [query, setQuery] = useState('');
if (!items?.length) {
return (
<EmptyState
variant="call-to-action"
message="You haven't created any repository configs yet"
button={
<LinkButton icon="plus" href={NEW_URL} size="lg">
Create repository config
</LinkButton>
}
/>
);
}
const filteredItems = items.filter((item) => item.metadata.name.includes(query));
return (
<Stack direction={'column'} gap={3}>
<Stack gap={2}>
<FilterInput placeholder="Search" value={query} onChange={setQuery} />
<LinkButton href={NEW_URL} variant="primary" icon={'plus'}>
Add repository config
</LinkButton>
</Stack>
<Stack direction={'column'}>
{!!filteredItems.length ? (
filteredItems.map((item) => {
return (
<Card key={item.metadata.name} className={css({ alignItems: 'center' })}>
<Stack direction={'column'}>
<Card.Heading>{item.spec.title}</Card.Heading>
<Card.Meta>{item.spec.type}</Card.Meta>
</Stack>
<Stack>
<LinkButton variant="secondary" href={`${PROVISIONING_URL}/edit/${item.metadata.name}`}>
Edit
</LinkButton>
<DeleteRepositoryButton name={item.metadata.name} />
</Stack>
</Card>
);
})
) : (
<EmptySearchResult>No results matching your query</EmptySearchResult>
)}
</Stack>
</Stack>
);
}
@@ -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<RequestOptions> {
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'],
});
@@ -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<RepositoryList, void>({
query: () => ({ url: BASE_PATH }),
providesTags: ['RepositoryList'],
}),
getRepository: build.query<RepositoryResource, { name: string }>({
query: ({ name }) => ({ url: `${BASE_PATH}/${name}` }),
}),
connectGetRepositoryHello: build.query<HelloWorld, { name: string; whom?: string }>({
query: ({ name, whom }) => ({
url: `${BASE_PATH}/${name}/hello`,
params: { whom },
}),
}),
// Mutations
createRepository: build.mutation<void, RepositoryForCreate>({
query: (resource) => ({
url: BASE_PATH,
method: 'POST',
body: resource,
}),
invalidatesTags: ['RepositoryList'],
}),
updateRepository: build.mutation<void, UpdateRequestArg>({
query: ({ name, body }) => ({
url: `${BASE_PATH}/${name}`,
method: 'PUT',
body,
}),
}),
deleteRepository: build.mutation<void, RequestArg>({
query: ({ name }) => ({
url: `${BASE_PATH}/${name}`,
method: 'DELETE',
}),
invalidatesTags: ['RepositoryList'],
}),
patchRepository: build.mutation<void, UpdateRequestArg>({
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;
@@ -0,0 +1,4 @@
export * from './endpoints';
import { generatedAPI } from './endpoints';
export const provisioningAPI = generatedAPI.enhanceEndpoints({});
@@ -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<RepositorySpec>;
export interface UpdateRequestArg extends RequestArg {
/** RepositorySpec */
body: ResourceForCreate<RepositorySpec>;
}
export type RepositoryList = ResourceList<RepositorySpec>;
export type RepositoryResource = Resource<RepositorySpec>;
export type HelloWorld = {
apiVersion?: string;
kind?: string;
whom?: string;
};
@@ -0,0 +1,2 @@
export const PROVISIONING_URL = '/admin/provisioning';
export const NEW_URL = `${PROVISIONING_URL}/new`;
+32
View File
@@ -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];
}
+10
View File
@@ -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';
};
@@ -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<RepositoryFormData> => {
return {
type: spec.type,
folder: spec.folder,
...spec.github,
...spec.local,
...spec.s3,
};
};
+18
View File
@@ -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(),
+2
View File
@@ -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<StoreState>) {
cloudMigrationAPI.middleware,
queryLibraryApi.middleware,
userPreferencesAPI.middleware,
provisioningAPI.middleware,
...extraMiddleware
),
devTools: process.env.NODE_ENV !== 'production',
+5
View File
@@ -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',
},
},
};