mirror of https://github.com/grafana/grafana.git
Provisioning: Use repository view rather than raw config (#103449)
This commit is contained in:
parent
8cd6f837a5
commit
8dbaeac9da
|
|
@ -3098,15 +3098,6 @@ exports[`better eslint`] = {
|
|||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/provisioning/hooks/index.ts:5381": [
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./useCreateOrUpdateRepositoryFile\`)", "0"],
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./useCreateOrUpdateRepository\`)", "1"],
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./useGetResourceRepository\`)", "2"],
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./useIsProvisionedNG\`)", "3"],
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./usePullRequestParam\`)", "4"],
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./useRepositoryJobs\`)", "5"],
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./useRepositoryList\`)", "6"]
|
||||
],
|
||||
"public/app/features/provisioning/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ type RepositoryView struct {
|
|||
// Repository display
|
||||
Title string `json:"title"`
|
||||
|
||||
// Edit options within the repository
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
|
||||
// The repository type
|
||||
Type RepositoryType `json:"type"`
|
||||
|
||||
// When syncing, where values are saved
|
||||
Target SyncTargetType `json:"target"`
|
||||
|
||||
// The supported workflows
|
||||
Workflows []Workflow `json:"workflows"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,6 +590,11 @@ func (in *RepositoryStatus) DeepCopy() *RepositoryStatus {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RepositoryView) DeepCopyInto(out *RepositoryView) {
|
||||
*out = *in
|
||||
if in.Workflows != nil {
|
||||
in, out := &in.Workflows, &out.Workflows
|
||||
*out = make([]Workflow, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -610,7 +615,9 @@ func (in *RepositoryViewList) DeepCopyInto(out *RepositoryViewList) {
|
|||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]RepositoryView, len(*in))
|
||||
copy(*out, *in)
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1212,14 +1212,6 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref common.ReferenceCa
|
|||
Format: "",
|
||||
},
|
||||
},
|
||||
"readOnly": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Edit options within the repository",
|
||||
Default: false,
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"type": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The repository type\n\nPossible enum values:\n - `\"github\"`\n - `\"local\"`",
|
||||
|
|
@ -1238,8 +1230,24 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryView(ref common.ReferenceCa
|
|||
Enum: []interface{}{"folder", "instance"},
|
||||
},
|
||||
},
|
||||
"workflows": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The supported workflows",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
Enum: []interface{}{"branch", "write"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "title", "readOnly", "type", "target"},
|
||||
Required: []string{"name", "title", "type", "target", "workflows"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provis
|
|||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ManagerStats,Stats
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositorySpec,Workflows
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryView,Workflows
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,RepositoryViewList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,ResourceList,Items
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1,TestResults,Errors
|
||||
|
|
|
|||
|
|
@ -163,11 +163,11 @@ func (b *APIBuilder) handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
for i, val := range all {
|
||||
settings.Items[i] = provisioning.RepositoryView{
|
||||
Name: val.ObjectMeta.Name,
|
||||
Title: val.Spec.Title,
|
||||
Type: val.Spec.Type,
|
||||
ReadOnly: len(val.Spec.Workflows) == 0,
|
||||
Target: val.Spec.Sync.Target,
|
||||
Name: val.ObjectMeta.Name,
|
||||
Title: val.Spec.Title,
|
||||
Type: val.Spec.Type,
|
||||
Target: val.Spec.Sync.Target,
|
||||
Workflows: val.Spec.Workflows,
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
|||
|
|
@ -3229,9 +3229,9 @@
|
|||
"required": [
|
||||
"name",
|
||||
"title",
|
||||
"readOnly",
|
||||
"type",
|
||||
"target"
|
||||
"target",
|
||||
"workflows"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
|
|
@ -3239,11 +3239,6 @@
|
|||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"readOnly": {
|
||||
"description": "Edit options within the repository",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"target": {
|
||||
"description": "When syncing, where values are saved\n\nPossible enum values:\n - `\"folder\"` Resources will be saved into a folder managed by this repository It will contain a copy of everything from the remote The folder k8s name will be the same as the repository k8s name\n - `\"instance\"` Resources are saved in the global context Only one repository may specify the `instance` target When this exists, the UI will promote writing to the instance repo rather than the grafana database (where possible)",
|
||||
"type": "string",
|
||||
|
|
@ -3266,6 +3261,18 @@
|
|||
"github",
|
||||
"local"
|
||||
]
|
||||
},
|
||||
"workflows": {
|
||||
"description": "The supported workflows",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"enum": [
|
||||
"branch",
|
||||
"write"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1124,8 +1124,6 @@ export type WebhookResponse = {
|
|||
export type RepositoryView = {
|
||||
/** The k8s name for this repository */
|
||||
name: string;
|
||||
/** Edit options within the repository */
|
||||
readOnly: boolean;
|
||||
/** When syncing, where values are saved
|
||||
|
||||
Possible enum values:
|
||||
|
|
@ -1140,6 +1138,8 @@ export type RepositoryView = {
|
|||
- `"github"`
|
||||
- `"local"` */
|
||||
type: 'github' | 'local';
|
||||
/** The supported workflows */
|
||||
workflows: ('branch' | 'write')[];
|
||||
};
|
||||
export type RepositoryViewList = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { getAppEvents } from '@grafana/runtime';
|
|||
import { useGetFolderQuery } from 'app/api/clients/folder';
|
||||
import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning';
|
||||
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
|
||||
import { usePullRequestParam, useRepositoryList } from 'app/features/provisioning/hooks';
|
||||
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
|
||||
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
|
||||
|
||||
import { FolderDTO } from '../../../types';
|
||||
|
||||
|
|
@ -46,10 +47,15 @@ jest.mock('app/api/clients/folder', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/provisioning/hooks', () => {
|
||||
jest.mock('app/features/provisioning/hooks/usePullRequestParam', () => {
|
||||
return {
|
||||
usePullRequestParam: jest.fn(),
|
||||
useRepositoryList: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () => {
|
||||
return {
|
||||
useGetResourceRepositoryView: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -127,25 +133,19 @@ describe('NewProvisionedFolderForm', () => {
|
|||
};
|
||||
(getAppEvents as jest.Mock).mockReturnValue(mockAppEvents);
|
||||
|
||||
(useRepositoryList as jest.Mock).mockReturnValue([
|
||||
[
|
||||
{
|
||||
metadata: {
|
||||
name: 'test-repo',
|
||||
},
|
||||
spec: {
|
||||
title: 'Test Repository',
|
||||
type: 'github',
|
||||
github: {
|
||||
url: 'https://github.com/grafana/grafana',
|
||||
branch: 'main',
|
||||
},
|
||||
workflows: [{ name: 'default', path: 'workflows/default.yaml' }],
|
||||
},
|
||||
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
repository: {
|
||||
name: 'test-repo',
|
||||
title: 'Test Repository',
|
||||
type: 'github',
|
||||
github: {
|
||||
url: 'https://github.com/grafana/grafana',
|
||||
branch: 'main',
|
||||
},
|
||||
],
|
||||
false,
|
||||
]);
|
||||
workflows: [{ name: 'default', path: 'workflows/default.json' }],
|
||||
},
|
||||
});
|
||||
|
||||
// Mock useGetFolderQuery
|
||||
(useGetFolderQuery as jest.Mock).mockReturnValue({
|
||||
|
|
@ -183,7 +183,9 @@ describe('NewProvisionedFolderForm', () => {
|
|||
});
|
||||
|
||||
it('should show loading state when repository data is loading', () => {
|
||||
(useRepositoryList as jest.Mock).mockReturnValue([[], true]);
|
||||
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
setup();
|
||||
|
||||
|
|
@ -191,7 +193,10 @@ describe('NewProvisionedFolderForm', () => {
|
|||
});
|
||||
|
||||
it('should show error when repository is not found', () => {
|
||||
(useRepositoryList as jest.Mock).mockReturnValue([null, false]);
|
||||
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
repository: undefined,
|
||||
});
|
||||
|
||||
setup();
|
||||
|
||||
|
|
@ -423,24 +428,18 @@ describe('NewProvisionedFolderForm', () => {
|
|||
|
||||
it('should show read-only alert when repository has no workflows', () => {
|
||||
// Mock repository with empty workflows array
|
||||
(useRepositoryList as jest.Mock).mockReturnValue([
|
||||
[
|
||||
{
|
||||
metadata: {
|
||||
name: 'test-repo',
|
||||
},
|
||||
spec: {
|
||||
type: 'github',
|
||||
github: {
|
||||
url: 'https://github.com/grafana/grafana',
|
||||
branch: 'main',
|
||||
},
|
||||
workflows: [], // Empty workflows array
|
||||
},
|
||||
(useGetResourceRepositoryView as jest.Mock).mockReturnValue({
|
||||
repository: {
|
||||
name: 'test-repo',
|
||||
title: 'Test Repository',
|
||||
type: 'github',
|
||||
github: {
|
||||
url: 'https://github.com/grafana/grafana',
|
||||
branch: 'main',
|
||||
},
|
||||
],
|
||||
false,
|
||||
]);
|
||||
workflows: [],
|
||||
},
|
||||
});
|
||||
|
||||
setup();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
|
@ -6,15 +5,15 @@ import { useNavigate } from 'react-router-dom-v5-compat';
|
|||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Alert, Button, Field, Input, RadioButtonGroup, Spinner, Stack, TextArea } from '@grafana/ui';
|
||||
import { useGetFolderQuery } from 'app/api/clients/folder';
|
||||
import { useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { AnnoKeyManagerIdentity, AnnoKeySourcePath, Resource } from 'app/features/apiserver/types';
|
||||
import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types';
|
||||
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults';
|
||||
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
|
||||
import { BranchValidationError } from 'app/features/provisioning/Shared/BranchValidationError';
|
||||
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
|
||||
import { usePullRequestParam, useRepositoryList } from 'app/features/provisioning/hooks';
|
||||
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
|
||||
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
|
||||
import { WorkflowOption } from 'app/features/provisioning/types';
|
||||
import { validateBranchName } from 'app/features/provisioning/utils/git';
|
||||
import { FolderDTO } from 'app/types';
|
||||
|
|
@ -41,26 +40,12 @@ const initialFormValues: Partial<FormData> = {
|
|||
};
|
||||
|
||||
export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: Props) {
|
||||
const [items, isLoading] = useRepositoryList();
|
||||
const { repository, folder, isLoading } = useGetResourceRepositoryView({ folderName: parentFolder?.uid });
|
||||
const prURL = usePullRequestParam();
|
||||
const navigate = useNavigate();
|
||||
const [create, request] = useCreateRepositoryFilesWithPathMutation();
|
||||
|
||||
// Get k8s folder data, necessary to get parent folder path
|
||||
const folderQuery = useGetFolderQuery(parentFolder ? { name: parentFolder.uid } : skipToken);
|
||||
const repositoryName = folderQuery.data?.metadata?.annotations?.[AnnoKeyManagerIdentity];
|
||||
if (!items && !isLoading) {
|
||||
return (
|
||||
<Alert
|
||||
title={t('browse-dashboards.new-provisioned-folder-form.title-repository-not-found', 'Repository not found')}
|
||||
severity="error"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const repository = repositoryName ? items?.find((item) => item?.metadata?.name === repositoryName) : items?.[0];
|
||||
const repositoryConfig = repository?.spec;
|
||||
const isGitHub = Boolean(repositoryConfig?.github);
|
||||
const isGitHub = Boolean(repository?.type === 'github');
|
||||
|
||||
const {
|
||||
register,
|
||||
|
|
@ -69,17 +54,17 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
|||
formState: { errors },
|
||||
control,
|
||||
setValue,
|
||||
} = useForm<FormData>({ defaultValues: { ...initialFormValues, workflow: getDefaultWorkflow(repositoryConfig) } });
|
||||
} = useForm<FormData>({ defaultValues: { ...initialFormValues, workflow: getDefaultWorkflow(repository) } });
|
||||
|
||||
const [workflow, ref] = watch(['workflow', 'ref']);
|
||||
|
||||
useEffect(() => {
|
||||
setValue('workflow', getDefaultWorkflow(repositoryConfig));
|
||||
}, [repositoryConfig, setValue]);
|
||||
setValue('workflow', getDefaultWorkflow(repository));
|
||||
}, [repository, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const appEvents = getAppEvents();
|
||||
if (request.isSuccess) {
|
||||
if (request.isSuccess && repository) {
|
||||
onSubmit();
|
||||
|
||||
appEvents.publish({
|
||||
|
|
@ -100,7 +85,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
|||
return;
|
||||
}
|
||||
|
||||
let url = `${PROVISIONING_URL}/${repositoryName}/file/${request.data.path}`;
|
||||
let url = `${PROVISIONING_URL}/${repository.name}/file/${request.data.path}`;
|
||||
if (request.data.ref?.length) {
|
||||
url += '?ref=' + request.data.ref;
|
||||
}
|
||||
|
|
@ -114,22 +99,21 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
|||
],
|
||||
});
|
||||
}
|
||||
}, [
|
||||
request.isSuccess,
|
||||
request.isError,
|
||||
request.error,
|
||||
onSubmit,
|
||||
ref,
|
||||
request.data,
|
||||
workflow,
|
||||
navigate,
|
||||
repositoryName,
|
||||
]);
|
||||
}, [request.isSuccess, request.isError, request.error, onSubmit, ref, request.data, workflow, navigate, repository]);
|
||||
|
||||
if (isLoading || folderQuery.isLoading) {
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!repository) {
|
||||
return (
|
||||
<Alert
|
||||
title={t('browse-dashboards.new-provisioned-folder-form.title-repository-not-found', 'Repository not found')}
|
||||
severity="error"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const validateFolderName = async (folderName: string) => {
|
||||
try {
|
||||
await validationSrv.validateNewFolderName(folderName);
|
||||
|
|
@ -143,11 +127,11 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
|||
};
|
||||
|
||||
const doSave = async ({ ref, title, workflow, comment }: FormData) => {
|
||||
const repoName = repository?.metadata?.name;
|
||||
const repoName = repository?.name;
|
||||
if (!title || !repoName) {
|
||||
return;
|
||||
}
|
||||
const basePath = folderQuery.data?.metadata?.annotations?.[AnnoKeySourcePath] ?? '';
|
||||
const basePath = folder?.metadata?.annotations?.[AnnoKeySourcePath] ?? '';
|
||||
|
||||
// Convert folder title to filename format (lowercase, replace spaces with hyphens)
|
||||
const titleInFilenameFormat = title
|
||||
|
|
@ -179,7 +163,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
|||
return (
|
||||
<form onSubmit={handleSubmit(doSave)}>
|
||||
<Stack direction="column" gap={2}>
|
||||
{!repositoryConfig?.workflows.length && (
|
||||
{!repository?.workflows?.length && (
|
||||
<Alert
|
||||
title={t(
|
||||
'browse-dashboards.new-provisioned-folder-form.title-this-repository-is-read-only',
|
||||
|
|
@ -229,7 +213,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
|||
control={control}
|
||||
name="workflow"
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<RadioButtonGroup {...field} options={getWorkflowOptions(repositoryConfig)} id={'folder-workflow'} />
|
||||
<RadioButtonGroup {...field} options={getWorkflowOptions(repository)} id={'folder-workflow'} />
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Save
|
|||
if (!defaultValues) {
|
||||
return null;
|
||||
}
|
||||
const { values, isNew, isGitHub, repositoryConfig } = defaultValues;
|
||||
const { values, isNew, isGitHub, repository } = defaultValues;
|
||||
|
||||
return (
|
||||
<SaveProvisionedDashboardForm
|
||||
|
|
@ -35,7 +35,7 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Save
|
|||
defaultValues={values}
|
||||
loadedFromRef={loadedFromRef}
|
||||
isGitHub={isGitHub}
|
||||
repositoryConfig={repositoryConfig}
|
||||
repository={repository}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,15 +48,9 @@ jest.mock('app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile', ()
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/provisioning/hooks/useGetResourceRepository', () => {
|
||||
jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () => {
|
||||
return {
|
||||
useGetResourceRepository: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/features/provisioning/hooks/useRepositoryList', () => {
|
||||
return {
|
||||
useRepositoryList: jest.fn(),
|
||||
useGetResourceRepositoryView: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -133,15 +127,12 @@ function setup(props: Partial<Props> = {}) {
|
|||
description: 'Test Description',
|
||||
workflow: 'write',
|
||||
},
|
||||
repositoryConfig: {
|
||||
repository: {
|
||||
name: 'repo-xyz',
|
||||
type: 'github',
|
||||
workflows: ['write', 'branch'],
|
||||
sync: { enabled: false, target: 'folder' },
|
||||
title: 'Test Repository',
|
||||
github: {
|
||||
branch: 'main',
|
||||
generateDashboardPreviews: false,
|
||||
},
|
||||
target: 'folder',
|
||||
},
|
||||
...props,
|
||||
};
|
||||
|
|
@ -401,10 +392,11 @@ describe('SaveProvisionedDashboardForm', () => {
|
|||
|
||||
it('should show read-only alert when repository has no workflows', () => {
|
||||
setup({
|
||||
repositoryConfig: {
|
||||
repository: {
|
||||
name: 'repo-abc',
|
||||
type: 'github',
|
||||
workflows: [],
|
||||
sync: { enabled: false, target: 'folder' },
|
||||
target: 'folder',
|
||||
title: 'Read-only Repository',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { AppEvents, locationUtil } from '@grafana/data';
|
|||
import { getAppEvents, locationService } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { Alert, Button, Field, Input, RadioButtonGroup, Stack, TextArea } from '@grafana/ui';
|
||||
import { RepositorySpec } from 'app/api/clients/provisioning';
|
||||
import { RepositoryView } from 'app/api/clients/provisioning';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
|
@ -43,7 +43,7 @@ export interface Props extends SaveProvisionedDashboardProps {
|
|||
isNew: boolean;
|
||||
defaultValues: FormData;
|
||||
isGitHub: boolean;
|
||||
repositoryConfig?: RepositorySpec;
|
||||
repository?: RepositoryView;
|
||||
loadedFromRef?: string;
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ export function SaveProvisionedDashboardForm({
|
|||
changeInfo,
|
||||
isNew,
|
||||
loadedFromRef,
|
||||
repositoryConfig,
|
||||
repository,
|
||||
isGitHub,
|
||||
}: Props) {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -154,12 +154,12 @@ export function SaveProvisionedDashboardForm({
|
|||
});
|
||||
};
|
||||
|
||||
const workflowOptions = getWorkflowOptions(repositoryConfig, loadedFromRef);
|
||||
const workflowOptions = getWorkflowOptions(repository, loadedFromRef);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} name="save-provisioned-form">
|
||||
<Stack direction="column" gap={2}>
|
||||
{!repositoryConfig?.workflows.length && (
|
||||
{!repository?.workflows?.length && (
|
||||
<Alert
|
||||
title={t(
|
||||
'dashboard-scene.save-provisioned-dashboard-form.title-this-repository-is-read-only',
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
import { RepositorySpec } from 'app/api/clients/provisioning';
|
||||
import { RepositoryView } from 'app/api/clients/provisioning';
|
||||
import { WorkflowOption } from 'app/features/provisioning/types';
|
||||
|
||||
export function getDefaultWorkflow(config?: RepositorySpec) {
|
||||
export function getDefaultWorkflow(config?: RepositoryView) {
|
||||
return config?.workflows?.[0];
|
||||
}
|
||||
|
||||
export function getWorkflowOptions(config?: RepositorySpec, ref?: string) {
|
||||
export function getWorkflowOptions(config?: RepositoryView, ref?: string) {
|
||||
if (!config) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (config.local?.path) {
|
||||
return [{ label: `Write to ${config.local.path}`, value: 'write' }];
|
||||
if (config.type === 'local') {
|
||||
return [{ label: `Save`, value: 'write' }];
|
||||
}
|
||||
|
||||
let branch = ref ?? config.github?.branch;
|
||||
const availableOptions: Array<{ label: string; value: WorkflowOption }> = [
|
||||
{ label: `Push to ${branch ?? 'main'}`, value: 'write' },
|
||||
{ label: ref ? `Push to ${ref}` : 'Save', value: 'write' },
|
||||
{ label: 'Push to different branch', value: 'branch' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { useGetFolderQuery } from 'app/api/clients/folder';
|
||||
import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath } from 'app/features/apiserver/types';
|
||||
import { useGetResourceRepository } from 'app/features/provisioning/hooks/useGetResourceRepository';
|
||||
import { useRepositoryList } from 'app/features/provisioning/hooks/useRepositoryList';
|
||||
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
|
||||
import { DashboardMeta } from 'app/types';
|
||||
|
||||
import { getDefaultWorkflow } from './defaults';
|
||||
|
|
@ -21,14 +17,13 @@ export function useDefaultValues({ meta, defaultTitle, defaultDescription }: Use
|
|||
const managerKind = annotations?.[AnnoKeyManagerKind];
|
||||
const managerIdentity = annotations?.[AnnoKeyManagerIdentity];
|
||||
const sourcePath = annotations?.[AnnoKeySourcePath];
|
||||
const repositoryConfig = useConfig({ folderUid: meta.folderUid, managerKind, managerIdentity });
|
||||
const repository = repositoryConfig?.spec;
|
||||
const { repository, folder, isLoading } = useGetResourceRepositoryView({
|
||||
name: managerKind === 'repo' ? managerIdentity : undefined,
|
||||
folderName: meta.folderUid,
|
||||
});
|
||||
const timestamp = generateTimestamp();
|
||||
|
||||
// Get folder data to retrieve the folder path
|
||||
const folderQuery = useGetFolderQuery(meta.folderUid ? { name: meta.folderUid } : skipToken);
|
||||
|
||||
const folderPath = meta.folderUid ? (folderQuery.data?.metadata?.annotations?.[AnnoKeySourcePath] ?? '') : '';
|
||||
const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath];
|
||||
|
||||
const dashboardPath = generatePath({
|
||||
timestamp,
|
||||
|
|
@ -37,7 +32,7 @@ export function useDefaultValues({ meta, defaultTitle, defaultDescription }: Use
|
|||
folderPath,
|
||||
});
|
||||
|
||||
if (folderQuery.isLoading || !repositoryConfig) {
|
||||
if (isLoading || !repository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +40,7 @@ export function useDefaultValues({ meta, defaultTitle, defaultDescription }: Use
|
|||
values: {
|
||||
ref: `dashboard/${timestamp}`,
|
||||
path: dashboardPath,
|
||||
repo: managerIdentity || repositoryConfig?.metadata?.name || '',
|
||||
repo: managerIdentity || repository?.name || '',
|
||||
comment: '',
|
||||
folder: {
|
||||
uid: meta.folderUid,
|
||||
|
|
@ -56,36 +51,7 @@ export function useDefaultValues({ meta, defaultTitle, defaultDescription }: Use
|
|||
workflow: getDefaultWorkflow(repository),
|
||||
},
|
||||
isNew: !meta.k8s?.name,
|
||||
repositoryConfig: repository,
|
||||
isGitHub: repository?.type === 'github',
|
||||
repository,
|
||||
};
|
||||
}
|
||||
|
||||
type UseConfigArgs = {
|
||||
folderUid?: string;
|
||||
managerKind?: string;
|
||||
managerIdentity?: string;
|
||||
};
|
||||
const useConfig = ({ folderUid, managerKind, managerIdentity }: UseConfigArgs) => {
|
||||
const repositoryConfig = useGetResourceRepository({
|
||||
name: managerKind === 'repo' ? managerIdentity : undefined,
|
||||
folderUid,
|
||||
});
|
||||
|
||||
const [items, isLoading] = useRepositoryList(repositoryConfig ? skipToken : undefined);
|
||||
|
||||
if (repositoryConfig) {
|
||||
return repositoryConfig;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
const instanceConfig = items?.find((repo) => repo.spec?.sync.target === 'instance');
|
||||
if (instanceConfig) {
|
||||
return instanceConfig;
|
||||
}
|
||||
|
||||
// Return the config, which targets the folder
|
||||
return items?.find((repo) => repo?.metadata?.name === folderUid);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
|
|||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
|
||||
import { useCreateOrUpdateRepository } from '../hooks';
|
||||
import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository';
|
||||
import { RepositoryFormData, WorkflowOption } from '../types';
|
||||
import { dataToSpec, specToData } from '../utils/data';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { t, Trans } from 'app/core/internationalization';
|
|||
import GettingStarted from './GettingStarted/GettingStarted';
|
||||
import GettingStartedPage from './GettingStarted/GettingStartedPage';
|
||||
import { FolderRepositoryList } from './Shared/FolderRepositoryList';
|
||||
import { useRepositoryList } from './hooks';
|
||||
import { useRepositoryList } from './hooks/useRepositoryList';
|
||||
|
||||
enum TabSelection {
|
||||
Repositories = 'repositories',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
import { useCreateOrUpdateRepository } from '../hooks';
|
||||
import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository';
|
||||
import { StepStatus } from '../hooks/useStepStatus';
|
||||
import { dataToSpec } from '../utils/data';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
export { useCreateOrUpdateRepository } from './useCreateOrUpdateRepository';
|
||||
export { useCreateOrUpdateRepositoryFile } from './useCreateOrUpdateRepositoryFile';
|
||||
export { useGetResourceRepository } from './useGetResourceRepository';
|
||||
export { useIsProvisionedNG } from './useIsProvisionedNG';
|
||||
export { usePullRequestParam } from './usePullRequestParam';
|
||||
export { useRepositoryJobs } from './useRepositoryJobs';
|
||||
export { useRepositoryList } from './useRepositoryList';
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { useGetFolderQuery } from '../../../api/clients/folder';
|
||||
import { AnnoKeyManagerIdentity } from '../../apiserver/types';
|
||||
|
||||
import { useRepositoryList } from './useRepositoryList';
|
||||
|
||||
interface GetResourceRepositoryArgs {
|
||||
name?: string;
|
||||
folderUid?: string;
|
||||
}
|
||||
|
||||
export const useGetResourceRepository = ({ name, folderUid }: GetResourceRepositoryArgs) => {
|
||||
const [items, isLoading] = useRepositoryList(name || !folderUid ? skipToken : undefined);
|
||||
// Get the folder data from API to get the repository data for nested folders
|
||||
const folderQuery = useGetFolderQuery(name || !folderUid ? skipToken : { name: folderUid });
|
||||
|
||||
const repoName = name || folderQuery.data?.metadata?.annotations?.[AnnoKeyManagerIdentity];
|
||||
|
||||
if (!items?.length || isLoading || !repoName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return items.find((repo) => repo.metadata?.name === repoName);
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { Folder, useGetFolderQuery } from 'app/api/clients/folder';
|
||||
import { RepositoryView, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning';
|
||||
import { AnnoKeyManagerIdentity } from 'app/features/apiserver/types';
|
||||
|
||||
interface GetResourceRepositoryArgs {
|
||||
name?: string; // the repository name
|
||||
folderName?: string; // folder we are targeting
|
||||
}
|
||||
|
||||
interface RepositoryViewData {
|
||||
repository?: RepositoryView;
|
||||
folder?: Folder;
|
||||
isLoading?: boolean;
|
||||
isInstanceManaged: boolean;
|
||||
}
|
||||
|
||||
// This is safe to call as a viewer (you do not need full access to the Repository configs)
|
||||
export const useGetResourceRepositoryView = ({ name, folderName }: GetResourceRepositoryArgs): RepositoryViewData => {
|
||||
const { data: settingsData, isLoading: isSettingsLoading } = useGetFrontendSettingsQuery();
|
||||
const skipFolderQuery = name || !folderName;
|
||||
const { data: folder, isLoading: isFolderLoading } = useGetFolderQuery(
|
||||
skipFolderQuery ? skipToken : { name: folderName }
|
||||
);
|
||||
|
||||
if (isSettingsLoading || isFolderLoading) {
|
||||
return { isLoading: true, isInstanceManaged: false };
|
||||
}
|
||||
|
||||
const items = settingsData?.items ?? [];
|
||||
|
||||
if (!items.length) {
|
||||
return { folder, isInstanceManaged: false };
|
||||
}
|
||||
|
||||
const instanceRepo = items.find((repo) => repo.target === 'instance');
|
||||
const isInstanceManaged = Boolean(instanceRepo);
|
||||
|
||||
if (name) {
|
||||
const repository = items.find((repo) => repo.name === name);
|
||||
if (repository) {
|
||||
return {
|
||||
repository,
|
||||
folder,
|
||||
isInstanceManaged,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Find the matching folder repository
|
||||
if (folderName) {
|
||||
// For root values it will be the same
|
||||
let repository = items.find((repo) => repo.name === folderName);
|
||||
if (repository) {
|
||||
return {
|
||||
repository,
|
||||
folder,
|
||||
isInstanceManaged,
|
||||
};
|
||||
}
|
||||
|
||||
// For nested folders we need to see what the folder thinks
|
||||
const annotatedFolderName = folder?.metadata?.annotations?.[AnnoKeyManagerIdentity];
|
||||
if (annotatedFolderName && name) {
|
||||
repository = items.find((repo) => repo.name === annotatedFolderName);
|
||||
if (repository) {
|
||||
return {
|
||||
repository,
|
||||
folder,
|
||||
isInstanceManaged,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
repository: instanceRepo,
|
||||
folder,
|
||||
isInstanceManaged,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,23 +1,17 @@
|
|||
import { config } from '@grafana/runtime';
|
||||
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning';
|
||||
|
||||
import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene';
|
||||
|
||||
import { useGetResourceRepository } from './useGetResourceRepository';
|
||||
import { useGetResourceRepositoryView } from './useGetResourceRepositoryView';
|
||||
|
||||
export function useIsProvisionedNG(dashboard: DashboardScene): boolean {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const folderUid = params.get('folderUid') || undefined;
|
||||
const folderName = params.get('folderUid') || undefined;
|
||||
|
||||
const folderRepository = useGetResourceRepository({ folderUid });
|
||||
const { data } = useGetFrontendSettingsQuery();
|
||||
const { repository, isInstanceManaged } = useGetResourceRepositoryView({ folderName });
|
||||
|
||||
if (!config.featureToggles.provisioning) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
dashboard.isManagedRepository() ||
|
||||
Boolean(folderRepository) ||
|
||||
Boolean(data?.items.some((item) => item.target === 'instance'))
|
||||
);
|
||||
return dashboard.isManagedRepository() || Boolean(repository) || isInstanceManaged;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue