Provisioning: Add translations (#103111)
This commit is contained in:
+1
-280
@@ -945,9 +945,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
],
|
||||
"public/app/core/components/NestedFolderPicker/FolderRepo.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
"public/app/core/components/OptionsUI/color.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
@@ -2521,27 +2518,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/browse-dashboards/api/browseDashboardsAPI.ts:5381": [
|
||||
[0, 0, 0, "Do not re-export imported variable (\`@reduxjs/toolkit/query/react\`)", "0"]
|
||||
],
|
||||
"public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "10"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "14"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "15"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "16"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "17"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "18"]
|
||||
],
|
||||
"public/app/features/browse-dashboards/state/index.ts:5381": [
|
||||
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
|
||||
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "1"],
|
||||
@@ -4318,264 +4294,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/profile/UserSessions.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/provisioning/Config/ConfigForm.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "10"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "11"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "12"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "13"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "14"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "15"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "16"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "17"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "18"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "19"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "20"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "21"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "22"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "23"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "24"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "25"]
|
||||
],
|
||||
"public/app/features/provisioning/Config/ConfigFormGithubCollapse.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"]
|
||||
],
|
||||
"public/app/features/provisioning/File/FileHistoryPage.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"]
|
||||
],
|
||||
"public/app/features/provisioning/File/FileStatusPage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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"]
|
||||
],
|
||||
"public/app/features/provisioning/File/FilesView.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/GettingStarted/EnhancedFeatures.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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"]
|
||||
],
|
||||
"public/app/features/provisioning/GettingStarted/FeaturesList.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"]
|
||||
],
|
||||
"public/app/features/provisioning/GettingStarted/GettingStarted.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/GettingStarted/SetupModal.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"]
|
||||
],
|
||||
"public/app/features/provisioning/HomePage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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/Job/JobStatus.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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"]
|
||||
],
|
||||
"public/app/features/provisioning/Job/RecentJobs.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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, "Unexpected any. Specify a different type.", "6"]
|
||||
],
|
||||
"public/app/features/provisioning/Repository/CheckRepository.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
],
|
||||
"public/app/features/provisioning/Repository/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/Repository/RepositoryActions.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/Repository/RepositoryCard.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"]
|
||||
],
|
||||
"public/app/features/provisioning/Repository/RepositoryHealth.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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/Repository/RepositoryOverview.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"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "13"]
|
||||
],
|
||||
"public/app/features/provisioning/Repository/RepositoryResources.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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/Repository/RepositoryStatusPage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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"]
|
||||
],
|
||||
"public/app/features/provisioning/Repository/SyncRepository.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/provisioning/Shared/FolderRepositoryList.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/provisioning/Shared/TokenPermissionsInfo.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/Wizard/BootstrapStep.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "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"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "11"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "12"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/ConnectStep.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "10"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "11"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "12"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "13"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "14"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/FinishStep.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/MigrateStep.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/PullStep.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/Stepper.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/provisioning/Wizard/WizardContent.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
[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"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { ManagerKind } from 'app/features/apiserver/types';
|
||||
import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance';
|
||||
import { NestedFolderDTO } from 'app/features/search/service/types';
|
||||
@@ -19,5 +20,5 @@ export function FolderRepo({ folder }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Badge text={'Provisioned'} color={'darkgrey'} />;
|
||||
return <Badge text={t('folder-repo.badge-text', 'Provisioned')} color={'darkgrey'} />;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ 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 { 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 { WorkflowOption } from 'app/features/provisioning/types';
|
||||
@@ -48,7 +50,12 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
const folderQuery = useGetFolderQuery(parentFolder ? { name: parentFolder.uid } : skipToken);
|
||||
const repositoryName = folderQuery.data?.metadata?.annotations?.[AnnoKeyManagerIdentity];
|
||||
if (!items && !isLoading) {
|
||||
return <Alert title="Repository not found" severity="error" />;
|
||||
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];
|
||||
@@ -77,9 +84,16 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Folder created successfully'],
|
||||
payload: [
|
||||
t(
|
||||
'browse-dashboards.new-provisioned-folder-form.alert-folder-created-successfully',
|
||||
'Folder created successfully'
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
// TODO: Update when the upsert type is fixed
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const folder = request.data.resource?.upsert as Resource;
|
||||
if (folder?.metadata?.name) {
|
||||
navigate(`/dashboards/f/${folder?.metadata?.name}/`);
|
||||
@@ -94,7 +108,10 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
} else if (request.isError) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: ['Error creating folder', request.error],
|
||||
payload: [
|
||||
t('browse-dashboards.new-provisioned-folder-form.alert-error-creating-folder', 'Error creating folder'),
|
||||
request.error,
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [
|
||||
@@ -121,7 +138,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
if (e instanceof Error) {
|
||||
return e.message;
|
||||
}
|
||||
return 'Invalid folder name';
|
||||
return t('browse-dashboards.new-provisioned-folder-form.error-invalid-folder-name', 'Invalid folder name');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,26 +180,43 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
<form onSubmit={handleSubmit(doSave)}>
|
||||
<Stack direction="column" gap={2}>
|
||||
{!repositoryConfig?.workflows.length && (
|
||||
<Alert title="This repository is read only">
|
||||
If you have direct access to the target, copy the JSON and paste it there.
|
||||
<Alert
|
||||
title={t(
|
||||
'browse-dashboards.new-provisioned-folder-form.title-this-repository-is-read-only',
|
||||
'This repository is read only'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="browse-dashboards.text-this-repository-is-read-only">
|
||||
If you have direct access to the target, copy the JSON and paste it there.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Field label="Folder name" invalid={!!errors.title} error={errors.title?.message}>
|
||||
<Field
|
||||
label={t('browse-dashboards.new-provisioned-folder-form.label-folder-name', 'Folder name')}
|
||||
invalid={!!errors.title}
|
||||
error={errors.title?.message}
|
||||
>
|
||||
<Input
|
||||
{...register('title', {
|
||||
required: 'Folder name is required',
|
||||
required: t('browse-dashboards.new-provisioned-folder-form.error-required', 'Folder name is required'),
|
||||
validate: validateFolderName,
|
||||
})}
|
||||
placeholder="Enter folder name"
|
||||
placeholder={t(
|
||||
'browse-dashboards.new-provisioned-folder-form.folder-name-input-placeholder-enter-folder-name',
|
||||
'Enter folder name'
|
||||
)}
|
||||
id="folder-name-input"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Comment">
|
||||
<Field label={t('browse-dashboards.new-provisioned-folder-form.label-comment', 'Comment')}>
|
||||
<TextArea
|
||||
{...register('comment')}
|
||||
placeholder="Add a note to describe your changes (optional)"
|
||||
placeholder={t(
|
||||
'browse-dashboards.new-provisioned-folder-form.folder-comment-input-placeholder-describe-changes-optional',
|
||||
'Add a note to describe your changes (optional)'
|
||||
)}
|
||||
id="folder-comment-input"
|
||||
rows={5}
|
||||
/>
|
||||
@@ -190,7 +224,7 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
|
||||
{isGitHub && (
|
||||
<>
|
||||
<Field label="Workflow">
|
||||
<Field label={t('browse-dashboards.new-provisioned-folder-form.label-workflow', 'Workflow')}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="workflow"
|
||||
@@ -201,8 +235,11 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
</Field>
|
||||
{workflow === 'branch' && (
|
||||
<Field
|
||||
label="Branch"
|
||||
description="Branch name in GitHub"
|
||||
label={t('browse-dashboards.new-provisioned-folder-form.label-branch', 'Branch')}
|
||||
description={t(
|
||||
'browse-dashboards.new-provisioned-folder-form.description-branch-name-in-git-hub',
|
||||
'Branch name in GitHub'
|
||||
)}
|
||||
invalid={!!errors?.ref}
|
||||
error={errors.ref ? <BranchValidationError /> : ''}
|
||||
>
|
||||
@@ -213,8 +250,16 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
)}
|
||||
|
||||
{prURL && (
|
||||
<Alert severity="info" title="Pull request created">
|
||||
A pull request has been created with changes to this folder:{' '}
|
||||
<Alert
|
||||
severity="info"
|
||||
title={t(
|
||||
'browse-dashboards.new-provisioned-folder-form.title-pull-request-created',
|
||||
'Pull request created'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="browse-dashboards.new-provisioned-folder-form.text-pull-request-created">
|
||||
A pull request has been created with changes to this folder:
|
||||
</Trans>{' '}
|
||||
<a href={prURL} target="_blank" rel="noopener noreferrer">
|
||||
{prURL}
|
||||
</a>
|
||||
@@ -223,27 +268,15 @@ export function NewProvisionedFolderForm({ onSubmit, onCancel, parentFolder }: P
|
||||
|
||||
<Stack gap={2}>
|
||||
<Button variant="secondary" fill="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
<Trans i18nKey="browse-dashboards.new-provisioned-folder-form.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" disabled={request.isLoading}>
|
||||
{request.isLoading ? 'Creating...' : 'Create'}
|
||||
{request.isLoading
|
||||
? t('browse-dashboards.new-provisioned-folder-form.button-creating', 'Creating...')
|
||||
: t('browse-dashboards.new-provisioned-folder-form.button-create', 'Create')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const BranchValidationError = () => {
|
||||
return (
|
||||
<>
|
||||
Invalid branch name.
|
||||
<ul style={{ padding: '0 20px' }}>
|
||||
<li>It cannot start with '/' or end with '/', '.', or whitespace.</li>
|
||||
<li>It cannot contain '//' or '..'.</li>
|
||||
<li>It cannot contain invalid characters: '~', '^', ':', '?', '*', '[', '\\', or ']'.</li>
|
||||
<li>It must have at least one valid character.</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-28
@@ -12,6 +12,7 @@ import { t, Trans } from 'app/core/internationalization';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { Resource } from 'app/features/apiserver/types';
|
||||
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 { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
|
||||
import { WorkflowOption } from 'app/features/provisioning/types';
|
||||
@@ -292,34 +293,6 @@ export function SaveProvisionedDashboardForm({
|
||||
);
|
||||
}
|
||||
|
||||
const BranchValidationError = () => (
|
||||
<>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.invalid-branch-name">Invalid branch name.</Trans>
|
||||
<ul style={{ padding: '0 20px' }}>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.cannot-start-with">
|
||||
It cannot start with '/' or end with '/', '.', or whitespace.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.it-cannot-contain-or">
|
||||
It cannot contain '//' or '..'.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.cannot-contain-invalid-characters">
|
||||
It cannot contain invalid characters: '~', '^', ':', '?', '*', '[', '\\', or ']'.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.least-valid-character">
|
||||
It must have at least one valid character.
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
|
||||
/**
|
||||
* Dashboard title validation to ensure it's not the same as the folder name
|
||||
* and meets other naming requirements.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@grafana/ui';
|
||||
import { Repository, RepositorySpec } from 'app/api/clients/provisioning';
|
||||
import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
|
||||
import { useCreateOrUpdateRepository } from '../hooks';
|
||||
@@ -27,16 +28,18 @@ import { dataToSpec, specToData } from '../utils/data';
|
||||
|
||||
import { ConfigFormGithubCollapse } from './ConfigFormGithubCollapse';
|
||||
|
||||
const typeOptions = ['GitHub', 'Local'].map((label) => ({ label, value: label.toLowerCase() }));
|
||||
const targetOptions = [
|
||||
{ value: 'instance', label: 'Entire instance' },
|
||||
{ value: 'folder', label: 'Managed folder' },
|
||||
];
|
||||
|
||||
export function getWorkflowOptions(type?: 'github' | 'local'): Array<ComboboxOption<WorkflowOption>> {
|
||||
const opts: Array<ComboboxOption<WorkflowOption>> = [
|
||||
{ label: 'Branch', value: 'branch', description: 'Create a branch (and pull request) for changes' },
|
||||
{ label: 'Write', value: 'write', description: 'Allow writing updates to the remote repository' },
|
||||
{
|
||||
label: t('provisioning.config-form.option-branch', 'Branch'),
|
||||
value: 'branch',
|
||||
description: t('provisioning.config-form.description-branch', 'Create a branch (and pull request) for changes'),
|
||||
},
|
||||
{
|
||||
label: t('provisioning.config-form.option-write', 'Write'),
|
||||
value: 'write',
|
||||
description: t('provisioning.config-form.description-write', 'Allow writing updates to the remote repository'),
|
||||
},
|
||||
];
|
||||
if (type === 'github') {
|
||||
return opts;
|
||||
@@ -87,13 +90,29 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
const navigate = useNavigate();
|
||||
const type = watch('type');
|
||||
|
||||
const typeOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'github', label: t('provisioning.config-form.option-github', 'GitHub') },
|
||||
{ value: 'local', label: t('provisioning.config-form.option-local', 'Local') },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const targetOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'instance', label: t('provisioning.config-form.option-entire-instance', 'Entire instance') },
|
||||
{ value: 'folder', label: t('provisioning.config-form.option-managed-folder', 'Managed folder') },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (request.isSuccess) {
|
||||
const formData = getValues();
|
||||
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Repository settings saved'],
|
||||
payload: [t('provisioning.config-form.alert-repository-settings-saved', 'Repository settings saved')],
|
||||
});
|
||||
reset(formData);
|
||||
setTimeout(() => {
|
||||
@@ -116,7 +135,7 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: 700 }}>
|
||||
<FormPrompt onDiscard={reset} confirmRedirect={isDirty} />
|
||||
<Field label={'Repository type'}>
|
||||
<Field label={t('provisioning.config-form.label-repository-type', 'Repository type')}>
|
||||
<Controller
|
||||
name={'type'}
|
||||
control={control}
|
||||
@@ -125,7 +144,7 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
<Combobox
|
||||
options={typeOptions}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
placeholder={'Select repository type'}
|
||||
placeholder={t('provisioning.config-form.placeholder-select-repository-type', 'Select repository type')}
|
||||
disabled={!!data?.spec}
|
||||
{...field}
|
||||
/>
|
||||
@@ -134,26 +153,41 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={'Title'}
|
||||
description={'A human-readable name for the config'}
|
||||
label={t('provisioning.config-form.label-title', 'Title')}
|
||||
description={t('provisioning.config-form.description-title', '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'} />
|
||||
<Input
|
||||
{...register('title', {
|
||||
required: t('provisioning.config-form.error-required', 'This field is required.'),
|
||||
})}
|
||||
placeholder={t('provisioning.config-form.placeholder-my-config', 'My config')}
|
||||
/>
|
||||
</Field>
|
||||
{type === 'github' && (
|
||||
<>
|
||||
<Field label={'GitHub token'} required error={errors?.token?.message} invalid={!!errors.token}>
|
||||
<Field
|
||||
label={t('provisioning.config-form.label-github-token', 'GitHub token')}
|
||||
required
|
||||
error={errors?.token?.message}
|
||||
invalid={!!errors.token}
|
||||
>
|
||||
<Controller
|
||||
name={'token'}
|
||||
control={control}
|
||||
rules={{ required: isEdit ? false : 'This field is required.' }}
|
||||
rules={{
|
||||
required: isEdit ? false : t('provisioning.config-form.error-required', 'This field is required.'),
|
||||
}}
|
||||
render={({ field: { ref, ...field } }) => {
|
||||
return (
|
||||
<SecretInput
|
||||
{...field}
|
||||
id={'token'}
|
||||
placeholder={'ghp_yourTokenHere1234567890abcdEFGHijklMNOP'}
|
||||
placeholder={t(
|
||||
'provisioning.config-form.placeholder-github-token',
|
||||
'ghp_yourTokenHere1234567890abcdEFGHijklMNOP'
|
||||
)}
|
||||
isConfigured={tokenConfigured}
|
||||
onReset={() => {
|
||||
setValue('token', '');
|
||||
@@ -166,53 +200,74 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
</Field>
|
||||
<TokenPermissionsInfo />
|
||||
<Field
|
||||
label={'Repository URL'}
|
||||
label={t('provisioning.config-form.label-repository-url', 'Repository URL')}
|
||||
error={errors?.url?.message}
|
||||
invalid={!!errors?.url}
|
||||
description={'Enter the GitHub repository URL'}
|
||||
description={t('provisioning.config-form.description-repository-url', 'Enter the GitHub repository URL')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
{...register('url', {
|
||||
required: 'This field is required.',
|
||||
required: t('provisioning.config-form.error-required', 'This field is required.'),
|
||||
pattern: {
|
||||
value: /^(?:https:\/\/github\.com\/)?[^/]+\/[^/]+$/,
|
||||
message: 'Please enter a valid GitHub repository URL',
|
||||
message: t(
|
||||
'provisioning.config-form.error-valid-github-url',
|
||||
'Please enter a valid GitHub repository URL'
|
||||
),
|
||||
},
|
||||
})}
|
||||
placeholder={'https://github.com/username/repo-name'}
|
||||
placeholder={t(
|
||||
'provisioning.config-form.placeholder-github-url',
|
||||
'https://github.com/username/repo-name'
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Branch'}>
|
||||
<Input {...register('branch')} placeholder={'main'} />
|
||||
<Field label={t('provisioning.config-form.label-branch', 'Branch')}>
|
||||
<Input {...register('branch')} placeholder={t('provisioning.config-form.placeholder-branch', 'main')} />
|
||||
</Field>
|
||||
<Field label={'Path'} description={'Path to a subdirectory in the Git repository'}>
|
||||
<Input {...register('path')} placeholder={'grafana/'} />
|
||||
<Field
|
||||
label={t('provisioning.config-form.label-path', 'Path')}
|
||||
description={t('provisioning.config-form.description-path', 'Path to a subdirectory in the Git repository')}
|
||||
>
|
||||
<Input {...register('path')} placeholder={t('provisioning.config-form.placeholder-path', 'grafana/')} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === '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
|
||||
label={t('provisioning.config-form.label-local-path', 'Local path')}
|
||||
error={errors?.path?.message}
|
||||
invalid={!!errors?.path}
|
||||
>
|
||||
<Input
|
||||
{...register('path', {
|
||||
required: t('provisioning.config-form.error-required', 'This field is required.'),
|
||||
})}
|
||||
placeholder={t('provisioning.config-form.placeholder-local-path', '/path/to/repo')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label={'Workflows'}
|
||||
label={t('provisioning.config-form.label-workflows', 'Workflows')}
|
||||
required
|
||||
error={errors?.workflows?.message}
|
||||
invalid={!!errors?.workflows}
|
||||
description="no workflows makes the repository read only"
|
||||
description={t(
|
||||
'provisioning.config-form.description-workflows-makes-repository',
|
||||
'No workflows makes the repository read only'
|
||||
)}
|
||||
>
|
||||
<Controller
|
||||
name={'workflows'}
|
||||
control={control}
|
||||
rules={{ required: 'This field is required.' }}
|
||||
rules={{ required: t('provisioning.config-form.error-required', 'This field is required.') }}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<MultiCombobox
|
||||
options={getWorkflowOptions(type)}
|
||||
placeholder={'Readonly repository'}
|
||||
placeholder={t('provisioning.config-form.placeholder-readonly-repository', 'Readonly repository')}
|
||||
onChange={(val) => {
|
||||
onChange(val.map((v) => v.value));
|
||||
}}
|
||||
@@ -228,15 +283,29 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<ControlledCollapse label="Automatic pulling" isOpen={false}>
|
||||
<Field label={'Enabled'} description={'Once automatic pulling is enabled, the target cannot be changed.'}>
|
||||
<ControlledCollapse
|
||||
label={t('provisioning.config-form.label-automatic-pulling', 'Automatic pulling')}
|
||||
isOpen={false}
|
||||
>
|
||||
<Field
|
||||
label={t('provisioning.config-form.label-enabled', 'Enabled')}
|
||||
description={t(
|
||||
'provisioning.config-form.description-enabled',
|
||||
'Once automatic pulling is enabled, the target cannot be changed.'
|
||||
)}
|
||||
>
|
||||
<Switch {...register('sync.enabled')} id={'sync.enabled'} />
|
||||
</Field>
|
||||
<Field label={'Target'} required error={errors?.sync?.target?.message} invalid={!!errors?.sync?.target}>
|
||||
<Field
|
||||
label={t('provisioning.config-form.label-target', 'Target')}
|
||||
required
|
||||
error={errors?.sync?.target?.message}
|
||||
invalid={!!errors?.sync?.target}
|
||||
>
|
||||
<Controller
|
||||
name={'sync.target'}
|
||||
control={control}
|
||||
rules={{ required: 'This field is required.' }}
|
||||
rules={{ required: t('provisioning.config-form.error-required', 'This field is required.') }}
|
||||
render={({ field: { ref, onChange, ...field } }) => {
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
@@ -249,14 +318,20 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Interval (seconds)'}>
|
||||
<Input {...register('sync.intervalSeconds', { valueAsNumber: true })} type={'number'} placeholder={'60'} />
|
||||
<Field label={t('provisioning.config-form.label-interval-seconds', 'Interval (seconds)')}>
|
||||
<Input
|
||||
{...register('sync.intervalSeconds', { valueAsNumber: true })}
|
||||
type={'number'}
|
||||
placeholder={t('provisioning.config-form.placeholder-interval-seconds', '60')}
|
||||
/>
|
||||
</Field>
|
||||
</ControlledCollapse>
|
||||
|
||||
<Stack gap={2}>
|
||||
<Button type={'submit'} disabled={request.isLoading}>
|
||||
{request.isLoading ? 'Saving...' : 'Save'}
|
||||
{request.isLoading
|
||||
? t('provisioning.config-form.button-saving', 'Saving...')
|
||||
: t('provisioning.config-form.button-save', 'Save')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, ControlledCollapse, Field } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { checkPublicAccess } from '../GettingStarted/features';
|
||||
import { GETTING_STARTED_URL } from '../constants';
|
||||
@@ -14,45 +15,77 @@ export function ConfigFormGithubCollapse({ previews }: ConfigFormGithubCollapseP
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<ControlledCollapse label="GitHub features" isOpen={true}>
|
||||
<h3>Realtime feedback</h3>
|
||||
<ControlledCollapse
|
||||
label={t('provisioning.config-form-github-collapse.label-git-hub-features', 'GitHub features')}
|
||||
isOpen={true}
|
||||
>
|
||||
<h3>
|
||||
<Trans i18nKey="provisioning.config-form-github-collapse.realtime-feedback">Realtime feedback</Trans>
|
||||
</h3>
|
||||
{checkPublicAccess() ? (
|
||||
<div>
|
||||
<Alert title={'Webhook will be created'} severity={'info'}>
|
||||
Changes in git will be quickly pulled into grafana. Pull requests can be processed.
|
||||
<Alert
|
||||
title={t(
|
||||
'provisioning.config-form-github-collapse.title-webhook-will-be-created',
|
||||
'Webhook will be created'
|
||||
)}
|
||||
severity={'info'}
|
||||
>
|
||||
<Trans i18nKey="provisioning.config-form-github-collapse.text-changes-in-git-quick-pull">
|
||||
Changes in git will be quickly pulled into grafana. Pull requests can be processed.
|
||||
</Trans>
|
||||
</Alert>
|
||||
</div>
|
||||
) : (
|
||||
<Alert
|
||||
title={'Public URL not configured'}
|
||||
title={t(
|
||||
'provisioning.config-form-github-collapse.title-public-url-not-configured',
|
||||
'Public URL not configured'
|
||||
)}
|
||||
severity={'warning'}
|
||||
buttonContent={<span>Instructions</span>}
|
||||
buttonContent={<Trans i18nKey="provisioning.config-form-github-collapse.instructions">Instructions</Trans>}
|
||||
onRemove={() => navigate(GETTING_STARTED_URL)}
|
||||
>
|
||||
Changes in git will eventually be pulled depending on the synchronization interval. Pull requests will not be
|
||||
processed
|
||||
<Trans i18nKey="provisioning.config-form-github-collapse.text-changes-in-git-eventually-pulled">
|
||||
Changes in git will eventually be pulled depending on the synchronization interval. Pull requests will not
|
||||
be processed
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<h3>Pull Request image previews</h3>
|
||||
<h3>
|
||||
<Trans i18nKey="provisioning.config-form-github-collapse.pull-request-image-previews">
|
||||
Pull Request image previews
|
||||
</Trans>
|
||||
</h3>
|
||||
{!config.rendererAvailable && (
|
||||
<Alert
|
||||
title={'Image renderer not configured'}
|
||||
title={t(
|
||||
'provisioning.config-form-github-collapse.title-image-renderer-not-configured',
|
||||
'Image renderer not configured'
|
||||
)}
|
||||
severity={'warning'}
|
||||
buttonContent={<span>Instructions</span>}
|
||||
buttonContent={<Trans i18nKey="provisioning.config-form-github-collapse.instructions">Instructions</Trans>}
|
||||
onRemove={() => window.open('https://grafana.com/grafana/plugins/grafana-image-renderer/', '_blank')}
|
||||
>
|
||||
When the image renderer is configured, pull requests can see preview images
|
||||
<Trans i18nKey="provisioning.config-form-github-collapse.text-when-image-renderer-configured">
|
||||
When the image renderer is configured, pull requests can see preview images
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label={'Attach dashboard previews to pull requests'}
|
||||
label={t(
|
||||
'provisioning.config-form-github-collapse.label-attach-dashboard-previews',
|
||||
'Attach dashboard previews to pull requests'
|
||||
)}
|
||||
description={
|
||||
<span>
|
||||
Render before/after images and link them to the pull request.
|
||||
<br />
|
||||
NOTE! this will render dashboards into an image that can be access by a public URL
|
||||
<Trans i18nKey="provisioning.config-form-github-collapse.description-attach-dashboard-previews">
|
||||
Render before/after images and link them to the pull request.
|
||||
<br />
|
||||
NOTE: This will render dashboards into an image that can be access by a public URL
|
||||
</Trans>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { Card, EmptyState, Spinner, Stack, Text, TextLink, UserIcon } from '@grafana/ui';
|
||||
import { useGetRepositoryHistoryWithPathQuery, useGetRepositoryStatusQuery } from 'app/api/clients/provisioning';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { isNotFoundError } from 'app/features/alerting/unified/api/util';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
@@ -28,8 +29,14 @@ export default function FileHistoryPage() {
|
||||
<Page.Contents isLoading={false}>
|
||||
{notFound ? (
|
||||
<EmptyState message={`Repository 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>
|
||||
<Text element={'p'}>
|
||||
<Trans i18nKey="provisioning.file-history-page.repository-config-exists-configuration">
|
||||
Make sure the repository config exists in the configuration file.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TextLink href={PROVISIONING_URL}>
|
||||
<Trans i18nKey="provisioning.file-history-page.back-to-repositories">Back to repositories</Trans>
|
||||
</TextLink>
|
||||
</EmptyState>
|
||||
) : (
|
||||
//@ts-expect-error TODO fix history response types
|
||||
@@ -48,7 +55,7 @@ interface Props {
|
||||
|
||||
function HistoryView({ history, path, repo }: Props) {
|
||||
if (!history.items) {
|
||||
return <div>not found</div>;
|
||||
return <Trans i18nKey="provisioning.history-view.not-found">Not found</Trans>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'app/api/clients/provisioning';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
|
||||
@@ -35,7 +36,11 @@ export default function FileStatusPage() {
|
||||
>
|
||||
<Page.Contents isLoading={file.isLoading}>
|
||||
<>
|
||||
{isFetchError(file.error) && <Alert title="Error loading file">{file.error.message}</Alert>}
|
||||
{isFetchError(file.error) && (
|
||||
<Alert title={t('provisioning.file-status-page.title-error-loading-file', 'Error loading file')}>
|
||||
{file.error.message}
|
||||
</Alert>
|
||||
)}
|
||||
{file.isSuccess && file.data && <ResourceView wrap={file.data} repo={name} repoRef={ref} tab={tab} />}
|
||||
</>
|
||||
</Page.Contents>
|
||||
@@ -91,24 +96,24 @@ function ResourceView({ wrap, repo, repoRef, tab }: Props) {
|
||||
<Stack>
|
||||
{isDashboard && (
|
||||
<LinkButton target={'_blank'} href={`${PROVISIONING_URL}/${repo}/dashboard/preview/${wrap.path}`}>
|
||||
Dashboard Preview
|
||||
<Trans i18nKey="provisioning.resource-view.dashboard-preview">Dashboard Preview</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
{isDashboard && existingName && (
|
||||
<LinkButton target={'_blank'} href={`d/${wrap.resource.existing?.metadata.name}`} variant="secondary">
|
||||
Existing dashboard
|
||||
<Trans i18nKey="provisioning.resource-view.existing-dashboard">Existing dashboard</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${repo}`} variant="secondary">
|
||||
Repository
|
||||
<Trans i18nKey="provisioning.resource-view.repository">Repository</Trans>
|
||||
</LinkButton>
|
||||
{repoRef && (
|
||||
<LinkButton href={`${PROVISIONING_URL}/${repo}/file/${wrap.path}`} variant="secondary">
|
||||
Base
|
||||
<Trans i18nKey="provisioning.resource-view.base">Base</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${repo}/history/${wrap.path}`} variant="secondary">
|
||||
History
|
||||
<Trans i18nKey="provisioning.resource-view.history">History</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
|
||||
@@ -171,7 +176,7 @@ function ResourceView({ wrap, repo, repoRef, tab }: Props) {
|
||||
/>
|
||||
</Stack>
|
||||
{replaceFileStatus.isError && (
|
||||
<Alert title="Error saving file">
|
||||
<Alert title={t('provisioning.resource-view.title-error-saving-file', 'Error saving file')}>
|
||||
<pre>{JSON.stringify(replaceFileStatus.error)}</pre>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
|
||||
import { CellProps, Column, FilterInput, InteractiveTable, LinkButton, Spinner, Stack } from '@grafana/ui';
|
||||
import { Repository, useGetRepositoryFilesQuery } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
import { FileDetails } from '../types';
|
||||
@@ -52,9 +53,13 @@ export function FilesView({ repo }: FilesViewProps) {
|
||||
return (
|
||||
<Stack>
|
||||
{(path.endsWith('.json') || path.endsWith('.yaml') || path.endsWith('.yml')) && (
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/file/${path}`}>View</LinkButton>
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/file/${path}`}>
|
||||
<Trans i18nKey="provisioning.files-view.columns.view">View</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/history/${path}`}>History</LinkButton>
|
||||
<LinkButton href={`${PROVISIONING_URL}/${name}/history/${path}`}>
|
||||
<Trans i18nKey="provisioning.files-view.columns.history">History</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
@@ -72,7 +77,12 @@ export function FilesView({ repo }: FilesViewProps) {
|
||||
return (
|
||||
<Stack grow={1} direction={'column'} gap={2}>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" autoFocus={true} value={searchQuery} onChange={setSearchQuery} />
|
||||
<FilterInput
|
||||
placeholder={t('provisioning.files-view.placeholder-search', 'Search')}
|
||||
autoFocus={true}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
</Stack>
|
||||
<InteractiveTable columns={columns} data={data} pageSize={25} getRowId={(f: FileDetails) => String(f.path)} />
|
||||
</Stack>
|
||||
|
||||
@@ -2,11 +2,12 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Box, Stack, Text, LinkButton, Icon, IconName, useStyles2 } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { FeatureCard } from './FeatureCard';
|
||||
|
||||
interface IconCircleProps {
|
||||
icon: string;
|
||||
icon: IconName;
|
||||
color: string;
|
||||
background: string;
|
||||
}
|
||||
@@ -20,7 +21,7 @@ const IconCircle = ({ icon, color, background }: IconCircleProps) => (
|
||||
width: `fit-content`,
|
||||
})}
|
||||
>
|
||||
<Icon name={icon as IconName} size="xxl" color={color} />
|
||||
<Icon name={icon} size="xxl" color={color} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,17 +36,27 @@ export const EnhancedFeatures = ({ hasPublicAccess, hasImageRenderer, onSetupPub
|
||||
|
||||
return (
|
||||
<Box marginTop={2}>
|
||||
<Text variant="h2">Unlock enhanced functionality for GitHub</Text>
|
||||
<Text variant="h2">
|
||||
<Trans i18nKey="provisioning.enhanced-features.unlock-enhanced-functionality-for-git-hub">
|
||||
Unlock enhanced functionality for GitHub
|
||||
</Trans>
|
||||
</Text>
|
||||
<Box marginTop={4}>
|
||||
<Stack direction="row" gap={2}>
|
||||
<FeatureCard
|
||||
title="Instant updates and pull requests with webhooks"
|
||||
description="Get instant updates in Grafana as soon as changes are committed. Review and approve changes using pull requests before they go live."
|
||||
title={t(
|
||||
'provisioning.enhanced-features.title-instant-updates-requests-webhooks',
|
||||
'Instant updates and pull requests with webhooks'
|
||||
)}
|
||||
description={t(
|
||||
'provisioning.enhanced-features.description-instant-updates',
|
||||
'Get instant updates in Grafana as soon as changes are committed. Review and approve changes using pull requests before they go live.'
|
||||
)}
|
||||
icon={<IconCircle icon="sync" color="primary" background="rgba(24, 121, 219, 0.12)" />}
|
||||
action={
|
||||
!hasPublicAccess && (
|
||||
<LinkButton fill="outline" onClick={onSetupPublicAccess}>
|
||||
Set up public access
|
||||
<Trans i18nKey="provisioning.enhanced-features.set-up-public-access">Set up public access</Trans>
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
@@ -54,8 +65,14 @@ export const EnhancedFeatures = ({ hasPublicAccess, hasImageRenderer, onSetupPub
|
||||
<div className={style.separator} />
|
||||
|
||||
<FeatureCard
|
||||
title="Visual previews in pull requests"
|
||||
description="See visual previews of dashboard updates directly in pull requests"
|
||||
title={t(
|
||||
'provisioning.enhanced-features.title-visual-previews-in-pull-requests',
|
||||
'Visual previews in pull requests'
|
||||
)}
|
||||
description={t(
|
||||
'provisioning.enhanced-features.description-visual-previews-dashboard-updates-directly-requests',
|
||||
'See visual previews of dashboard updates directly in pull requests'
|
||||
)}
|
||||
icon={
|
||||
<Stack direction="row" gap={2}>
|
||||
<IconCircle icon="camera" color="orange" background="rgba(255, 120, 10, 0.12)" />
|
||||
@@ -69,7 +86,7 @@ export const EnhancedFeatures = ({ hasPublicAccess, hasImageRenderer, onSetupPub
|
||||
href="https://grafana.com/grafana/plugins/grafana-image-renderer/"
|
||||
icon="external-link-alt"
|
||||
>
|
||||
Set up image rendering
|
||||
<Trans i18nKey="provisioning.enhanced-features.set-up-image-rendering">Set up image rendering</Trans>
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Stack, Text, Box, LinkButton, Icon } from '@grafana/ui';
|
||||
import { Repository } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { ConnectRepositoryButton } from '../Shared/ConnectRepositoryButton';
|
||||
|
||||
interface FeatureItemProps {
|
||||
children: NonNullable<React.ReactNode>;
|
||||
children: NonNullable<ReactNode>;
|
||||
}
|
||||
|
||||
const FeatureItem = ({ children }: FeatureItemProps) => {
|
||||
@@ -37,7 +40,9 @@ export const FeaturesList = ({
|
||||
return (
|
||||
<Box>
|
||||
<LinkButton fill="outline" onClick={onSetupFeatures}>
|
||||
Set up required feature toggles
|
||||
<Trans i18nKey="provisioning.features-list.actions.set-up-required-feature-toggles">
|
||||
Set up required feature toggles
|
||||
</Trans>
|
||||
</LinkButton>
|
||||
</Box>
|
||||
);
|
||||
@@ -52,25 +57,45 @@ export const FeaturesList = ({
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text variant="h2">Manage your dashboards with remote provisioning</Text>
|
||||
<FeatureItem>Manage dashboards as code and provision updates automatically</FeatureItem>
|
||||
<Text variant="h2">
|
||||
<Trans i18nKey="provisioning.features-list.manage-your-dashboards-with-remote-provisioning">
|
||||
Manage your dashboards with remote provisioning
|
||||
</Trans>
|
||||
</Text>
|
||||
<FeatureItem>
|
||||
Store dashboards in version-controlled storage for better organization and history tracking
|
||||
<Trans i18nKey="provisioning.features-list.manage-dashboards-provision-updates-automatically">
|
||||
Manage dashboards as code and provision updates automatically
|
||||
</Trans>
|
||||
</FeatureItem>
|
||||
<FeatureItem>
|
||||
<Trans i18nKey="provisioning.features-list.store-dashboards-in-version-controlled-storage">
|
||||
Store dashboards in version-controlled storage for better organization and history tracking
|
||||
</Trans>
|
||||
</FeatureItem>
|
||||
<FeatureItem>
|
||||
<Trans i18nKey="provisioning.features-list.migrate-existing-dashboards-storage-provisioning">
|
||||
Migrate existing dashboards to storage for provisioning
|
||||
</Trans>
|
||||
</FeatureItem>
|
||||
<FeatureItem>Migrate existing dashboards to storage for provisioning</FeatureItem>
|
||||
{hasPublicAccess && (
|
||||
<FeatureItem>
|
||||
Automatically provision and update your dashboards as soon as changes are pushed to your GitHub repository
|
||||
<Trans i18nKey="provisioning.features-list.automatically-provision-and-update-dashboards">
|
||||
Automatically provision and update your dashboards as soon as changes are pushed to your GitHub repository
|
||||
</Trans>
|
||||
</FeatureItem>
|
||||
)}
|
||||
{hasImageRenderer && hasPublicAccess && (
|
||||
<FeatureItem>Visual previews in pull requests to review your changes before going live</FeatureItem>
|
||||
<FeatureItem>
|
||||
<Trans i18nKey="provisioning.features-list.visual-previews-in-pull-requests">
|
||||
Visual previews in pull requests to review your changes before going live
|
||||
</Trans>
|
||||
</FeatureItem>
|
||||
)}
|
||||
|
||||
{false && (
|
||||
// We haven't gotten the design for this quite yet.
|
||||
<LinkButton fill="text" href="#" icon="external-link-alt">
|
||||
Learn more
|
||||
<Trans i18nKey="provisioning.features-list.learn-more">Learn more</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import { Alert, Stack, Text, Box } from '@grafana/ui';
|
||||
import { useGetFrontendSettingsQuery, Repository } from 'app/api/clients/provisioning';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { EnhancedFeatures } from './EnhancedFeatures';
|
||||
import { FeaturesList } from './FeaturesList';
|
||||
@@ -63,35 +64,62 @@ export default function GettingStarted({ items }: Props) {
|
||||
switch (setupType) {
|
||||
case 'public-access':
|
||||
return {
|
||||
title: 'Set up public access',
|
||||
description: 'Set up public access to your Grafana instance to enable GitHub integration',
|
||||
title: t('provisioning.getting-started.modal-title-set-up-public-access', 'Set up public access'),
|
||||
description: t(
|
||||
'provisioning.getting-started.modal-description-public-access',
|
||||
'Set up public access to your Grafana instance to enable GitHub integration'
|
||||
),
|
||||
steps: [
|
||||
{
|
||||
title: 'Start ngrok for temporary public access',
|
||||
description: 'Run this command to create a secure tunnel to your local Grafana:',
|
||||
title: t(
|
||||
'provisioning.getting-started.step-title-start-ngrok',
|
||||
'Start ngrok for temporary public access'
|
||||
),
|
||||
description: t(
|
||||
'provisioning.getting-started.step-description-start-ngrok',
|
||||
'Run this command to create a secure tunnel to your local Grafana:'
|
||||
),
|
||||
code: 'ngrok http 3000',
|
||||
},
|
||||
{
|
||||
title: 'Copy your public URL',
|
||||
description: 'From the ngrok output, copy the https:// forwarding URL that looks like this:',
|
||||
title: t('provisioning.getting-started.step-title-copy-url', 'Copy your public URL'),
|
||||
description: t(
|
||||
'provisioning.getting-started.step-description-copy-url',
|
||||
'From the ngrok output, copy the https:// forwarding URL that looks like this:'
|
||||
),
|
||||
code: ngrokExample,
|
||||
copyCode: false,
|
||||
},
|
||||
{
|
||||
title: 'Update your Grafana configuration',
|
||||
description: 'Add this to your custom.ini file, replacing the URL with your actual ngrok URL:',
|
||||
title: t(
|
||||
'provisioning.getting-started.step-title-update-grafana-config',
|
||||
'Update your Grafana configuration'
|
||||
),
|
||||
description: t(
|
||||
'provisioning.getting-started.step-description-update-grafana-config',
|
||||
'Add this to your custom.ini file, replacing the URL with your actual ngrok URL:'
|
||||
),
|
||||
code: rootUrlExample,
|
||||
},
|
||||
],
|
||||
};
|
||||
case 'required-features':
|
||||
return {
|
||||
title: 'Set up required features',
|
||||
description: 'Enable required Grafana features for provisioning',
|
||||
title: t('provisioning.getting-started.modal-title-set-up-required-features', 'Set up required features'),
|
||||
description: t(
|
||||
'provisioning.getting-started.modal-description-required-features',
|
||||
'Enable required Grafana features for provisioning'
|
||||
),
|
||||
steps: [
|
||||
{
|
||||
title: 'Enable Required Feature Toggles',
|
||||
description: 'Add these settings to your custom.ini file to enable necessary features:',
|
||||
title: t(
|
||||
'provisioning.getting-started.step-title-enable-feature-toggles',
|
||||
'Enable Required Feature Toggles'
|
||||
),
|
||||
description: t(
|
||||
'provisioning.getting-started.step-description-enable-feature-toggles',
|
||||
'Add these settings to your custom.ini file to enable necessary features:'
|
||||
),
|
||||
code: featureIni,
|
||||
},
|
||||
],
|
||||
@@ -108,9 +136,17 @@ export default function GettingStarted({ items }: Props) {
|
||||
return (
|
||||
<>
|
||||
{legacyStorage && (
|
||||
<Alert severity="info" title="Setting up this connection could cause a temporary outage">
|
||||
When you connect your whole instance, dashboards will be unavailable while running the migration. We recommend
|
||||
warning your users before starting the process.
|
||||
<Alert
|
||||
severity="info"
|
||||
title={t(
|
||||
'provisioning.getting-started.title-setting-connection-could-cause-temporary-outage',
|
||||
'Setting up this connection could cause a temporary outage'
|
||||
)}
|
||||
>
|
||||
<Trans i18nKey="provisioning.getting-started.alert-temporary-outage">
|
||||
When you connect your whole instance, dashboards will be unavailable while running the migration. We
|
||||
recommend warning your users before starting the process.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
<Stack direction="row" gap={2}>
|
||||
@@ -138,7 +174,9 @@ export default function GettingStarted({ items }: Props) {
|
||||
justifyContent: `center`,
|
||||
})}
|
||||
>
|
||||
<Text variant="h2">Engaging graphic</Text>
|
||||
<Text variant="h2">
|
||||
<Trans i18nKey="provisioning.getting-started.engaging-graphic">Engaging graphic</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Repository } from 'app/api/clients/provisioning';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import GettingStarted from './GettingStarted';
|
||||
interface Props {
|
||||
@@ -11,9 +12,11 @@ export default function GettingStartedPage({ items }: Props) {
|
||||
<Page
|
||||
navId="provisioning"
|
||||
pageNav={{
|
||||
text: 'Remote provisioning',
|
||||
subTitle:
|
||||
'Provisioning is a feature that allows you to manage your dashboards using GitHub and other storage systems',
|
||||
text: t('provisioning.getting-started-page.text-remote-provisioning', 'Remote provisioning'),
|
||||
subTitle: t(
|
||||
'provisioning.getting-started-page.subtitle-provisioning-feature',
|
||||
'Provisioning is a feature that allows you to manage your dashboards using GitHub and other storage systems'
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Page.Contents>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Modal, Button, useStyles2, Stack, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { SetupStep } from './SetupStep';
|
||||
import { Sidebar } from './Sidebar';
|
||||
@@ -47,16 +48,16 @@ export const SetupModal = ({ title, description, steps, isOpen, onDismiss }: Pro
|
||||
<Modal.ButtonRow>
|
||||
<Stack direction="row" justifyContent="flex-end" gap={2}>
|
||||
<Button variant="secondary" onClick={handlePrevious} disabled={isFirstStep}>
|
||||
Previous
|
||||
<Trans i18nKey="provisioning.setup-modal.previous">Previous</Trans>
|
||||
</Button>
|
||||
|
||||
{isLastStep ? (
|
||||
<Button variant="primary" onClick={onDismiss} icon="check-circle">
|
||||
Done
|
||||
<Trans i18nKey="provisioning.setup-modal.done">Done</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" onClick={handleNext}>
|
||||
Next
|
||||
<Trans i18nKey="provisioning.setup-modal.next">Next</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, Text, Stack, Card } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
export interface Props {
|
||||
step: string;
|
||||
@@ -18,12 +19,24 @@ export const SidebarItem = ({ step, index, currentStep, onStepClick, styles }: P
|
||||
|
||||
const getStepStatus = () => {
|
||||
if (isCompleted) {
|
||||
return { icon: 'check-circle' as const, color: 'success', label: 'Completed step' };
|
||||
return {
|
||||
icon: 'check-circle' as const,
|
||||
color: 'success',
|
||||
label: t('provisioning.sidebar-item.label-completed-step', 'Completed step'),
|
||||
};
|
||||
}
|
||||
if (isCurrent) {
|
||||
return { icon: 'circle' as const, color: 'primary', label: 'Current step' };
|
||||
return {
|
||||
icon: 'circle' as const,
|
||||
color: 'primary',
|
||||
label: t('provisioning.sidebar-item.label-current-step', 'Current step'),
|
||||
};
|
||||
}
|
||||
return { icon: 'circle' as const, color: 'secondary', label: 'Pending step' };
|
||||
return {
|
||||
icon: 'circle' as const,
|
||||
color: 'secondary',
|
||||
label: t('provisioning.sidebar-item.label-pending-step', 'Pending step'),
|
||||
};
|
||||
};
|
||||
|
||||
const { icon, color, label } = getStepStatus();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Alert, ConfirmModal, Stack, Tab, TabContent, TabsBar } from '@grafana/ui';
|
||||
import { useDeletecollectionRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { FilesView } from './File/FilesView';
|
||||
import GettingStarted from './GettingStarted/GettingStarted';
|
||||
@@ -26,18 +27,6 @@ enum TabSelection {
|
||||
Repositories = 'repositories',
|
||||
}
|
||||
|
||||
const connectedTabInfo = [
|
||||
{ value: TabSelection.Overview, label: 'Overview', title: 'Repository overview' },
|
||||
{ value: TabSelection.Resources, label: 'Resources', title: 'Resources saved in Grafana database' },
|
||||
{ value: TabSelection.Files, label: 'Files', title: 'The raw file list from the repository' },
|
||||
{ value: TabSelection.GettingStarted, label: 'Getting started', title: 'Getting started' },
|
||||
];
|
||||
|
||||
const disconnectedTabInfo = [
|
||||
{ value: TabSelection.Repositories, label: 'Repositories', title: 'List of repositories' },
|
||||
{ value: TabSelection.GettingStarted, label: 'Getting started', title: 'Getting started' },
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const [items, isLoading] = useRepositoryList({ watch: true });
|
||||
const settings = useGetFrontendSettingsQuery();
|
||||
@@ -48,11 +37,53 @@ export default function HomePage() {
|
||||
instanceConnected ? TabSelection.Overview : TabSelection.Repositories
|
||||
);
|
||||
|
||||
const connectedTabInfo = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: TabSelection.Overview,
|
||||
label: t('provisioning.home-page.tab-overview', 'Overview'),
|
||||
title: t('provisioning.home-page.tab-overview-title', 'Repository overview'),
|
||||
},
|
||||
{
|
||||
value: TabSelection.Resources,
|
||||
label: t('provisioning.home-page.tab-resources', 'Resources'),
|
||||
title: t('provisioning.home-page.tab-resources-title', 'Resources saved in Grafana database'),
|
||||
},
|
||||
{
|
||||
value: TabSelection.Files,
|
||||
label: t('provisioning.home-page.tab-files', 'Files'),
|
||||
title: t('provisioning.home-page.tab-files-title', 'The raw file list from the repository'),
|
||||
},
|
||||
{
|
||||
value: TabSelection.GettingStarted,
|
||||
label: t('provisioning.home-page.tab-getting-started', 'Getting started'),
|
||||
title: t('provisioning.home-page.tab-getting-started-title', 'Getting started'),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const disconnectedTabInfo = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: TabSelection.Repositories,
|
||||
label: t('provisioning.home-page.tab-repositories', 'Repositories'),
|
||||
title: t('provisioning.home-page.tab-repositories-title', 'List of repositories'),
|
||||
},
|
||||
{
|
||||
value: TabSelection.GettingStarted,
|
||||
label: t('provisioning.home-page.tab-getting-started', 'Getting started'),
|
||||
title: t('provisioning.home-page.tab-getting-started-title', 'Getting started'),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteAllResult.isSuccess) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['All configured repositories deleted'],
|
||||
payload: [t('provisioning.home-page.success-all-repositories-deleted', 'All configured repositories deleted')],
|
||||
});
|
||||
}
|
||||
}, [deleteAllResult.isSuccess]);
|
||||
@@ -105,27 +136,39 @@ export default function HomePage() {
|
||||
return (
|
||||
<Page
|
||||
navId="provisioning"
|
||||
subTitle="View and manage your configured repositories"
|
||||
subTitle={t('provisioning.home-page.subtitle', 'View and manage your configured repositories')}
|
||||
actions={instanceConnected && items?.length ? <RepositoryActions repository={items[0]} /> : undefined}
|
||||
>
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
{settings.data?.legacyStorage && (
|
||||
<Alert
|
||||
title="Legacy storage detected"
|
||||
title={t('provisioning.home-page.title-legacy-storage-detected', 'Legacy storage detected')}
|
||||
severity="error"
|
||||
buttonContent={<>Remove all configured repositories</>}
|
||||
buttonContent={
|
||||
<Trans i18nKey="provisioning.home-page.remove-all-configured-repositories">
|
||||
Remove all configured repositories
|
||||
</Trans>
|
||||
}
|
||||
onRemove={() => {
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
Configured repositories will not work while running legacy storage.
|
||||
<Trans i18nKey="provisioning.home-page.configured-repositories-while-running-legacy-storage">
|
||||
Configured repositories will not work while running legacy storage.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
<ConfirmModal
|
||||
isOpen={showDeleteModal}
|
||||
title="Delete all configured repositories"
|
||||
body="Are you sure you want to delete all configured repositories? This action cannot be undone."
|
||||
confirmText="Delete repositories"
|
||||
title={t(
|
||||
'provisioning.home-page.title-delete-all-configured-repositories',
|
||||
'Delete all configured repositories'
|
||||
)}
|
||||
body={t(
|
||||
'provisioning.home-page.confirm-delete-repositories',
|
||||
'Are you sure you want to delete all configured repositories? This action cannot be undone.'
|
||||
)}
|
||||
confirmText={t('provisioning.home-page.button-delete-repositories', 'Delete repositories')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onDismiss={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||
|
||||
import { Alert, ControlledCollapse, LinkButton, Spinner, Stack, Text } from '@grafana/ui';
|
||||
import { useGetRepositoryQuery } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import ProgressBar from '../Shared/ProgressBar';
|
||||
import { useRepositoryAllJobs } from '../hooks/useRepositoryAllJobs';
|
||||
@@ -41,7 +42,7 @@ export function JobStatus({ name, onStatusChange, onRunningChange, onErrorChange
|
||||
}
|
||||
}
|
||||
if (onErrorChange && job?.status?.state === 'error') {
|
||||
onErrorChange(job.status.message ?? 'An unknown error occurred');
|
||||
onErrorChange(job.status.message ?? t('provisioning.job-status.error-unknown', 'An unknown error occurred'));
|
||||
if (onRunningChange) {
|
||||
onRunningChange(false);
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export function JobStatus({ name, onStatusChange, onRunningChange, onErrorChange
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
<Spinner size={24} />
|
||||
<Text element="h4" weight="bold">
|
||||
Starting...
|
||||
<Trans i18nKey="provisioning.job-status.starting">Starting...</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -62,10 +63,18 @@ export function JobStatus({ name, onStatusChange, onRunningChange, onErrorChange
|
||||
const status = () => {
|
||||
switch (job.status?.state) {
|
||||
case 'success':
|
||||
return <Alert severity="success" title="Job completed successfully" />;
|
||||
return (
|
||||
<Alert
|
||||
severity="success"
|
||||
title={t('provisioning.job-status.status.title-job-completed-successfully', 'Job completed successfully')}
|
||||
/>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Alert severity="error" title="error running job">
|
||||
<Alert
|
||||
severity="error"
|
||||
title={t('provisioning.job-status.status.title-error-running-job', 'error running job')}
|
||||
>
|
||||
{job.status.message}
|
||||
</Alert>
|
||||
);
|
||||
@@ -74,7 +83,7 @@ export function JobStatus({ name, onStatusChange, onRunningChange, onErrorChange
|
||||
<Stack direction="row" alignItems="center" justifyContent="center" gap={2}>
|
||||
{!job.status?.progress && <Spinner size={24} />}
|
||||
<Text element="h4" color="secondary">
|
||||
{job.status?.message ?? job.status?.state!}
|
||||
{job.status?.message ?? job.status?.state ?? ''}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -92,14 +101,16 @@ export function JobStatus({ name, onStatusChange, onRunningChange, onErrorChange
|
||||
|
||||
{job.status.summary && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text variant="h3">Summary</Text>
|
||||
<Text variant="h3">
|
||||
<Trans i18nKey="provisioning.job-status.summary">Summary</Trans>
|
||||
</Text>
|
||||
<JobSummary summary={job.status.summary} />
|
||||
</Stack>
|
||||
)}
|
||||
{job.status.state === 'success' ? (
|
||||
<RepositoryLink name={job.metadata?.labels?.repository} />
|
||||
) : (
|
||||
<ControlledCollapse label="View details" isOpen={false}>
|
||||
<ControlledCollapse label={t('provisioning.job-status.label-view-details', 'View details')} isOpen={false}>
|
||||
<pre>{JSON.stringify(job, null, ' ')}</pre>
|
||||
</ControlledCollapse>
|
||||
)}
|
||||
@@ -130,13 +141,17 @@ function RepositoryLink({ name }: RepositoryLinkProps) {
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={1}>
|
||||
<Text>Grafana and your repository are now in sync.</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="provisioning.repository-link.grafana-repository">
|
||||
Grafana and your repository are now in sync.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={2}>
|
||||
<LinkButton fill="outline" href={repoHref} icon="external-link-alt" target="_blank" rel="noopener noreferrer">
|
||||
View repository
|
||||
<Trans i18nKey="provisioning.repository-link.view-repository">View repository</Trans>
|
||||
</LinkButton>
|
||||
<LinkButton fill="outline" href={folderHref} icon="folder-open">
|
||||
View folder
|
||||
<Trans i18nKey="provisioning.repository-link.view-folder">View folder</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
|
||||
import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data';
|
||||
import { Alert, Badge, Box, Card, Icon, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui';
|
||||
import { HistoricJob, Job, Repository, SyncStatus } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import KeyValuesTable from 'app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable';
|
||||
|
||||
import { useRepositoryAllJobs } from '../hooks/useRepositoryAllJobs';
|
||||
@@ -37,7 +38,7 @@ const getStatusColor = (state?: SyncStatus['state']) => {
|
||||
const getJobColumns = () => [
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
header: t('provisioning.recent-jobs.column-status', 'Status'),
|
||||
cell: ({ row: { original: job } }: JobCell) => (
|
||||
<Badge
|
||||
text={job.status?.state || ''}
|
||||
@@ -48,17 +49,17 @@ const getJobColumns = () => [
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
header: 'Action',
|
||||
header: t('provisioning.recent-jobs.column-action', 'Action'),
|
||||
cell: ({ row: { original: job } }: JobCell) => job.spec?.action,
|
||||
},
|
||||
{
|
||||
id: 'started',
|
||||
header: 'Started',
|
||||
header: t('provisioning.recent-jobs.column-started', 'Started'),
|
||||
cell: ({ row: { original: job } }: JobCell) => formatTimestamp(job.status?.started),
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
header: 'Duration',
|
||||
header: t('provisioning.recent-jobs.column-duration', 'Duration'),
|
||||
cell: ({ row: { original: job } }: JobCell) => {
|
||||
const interval = {
|
||||
start: job.status?.started ?? 0,
|
||||
@@ -76,7 +77,7 @@ const getJobColumns = () => [
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
header: 'Message',
|
||||
header: t('provisioning.recent-jobs.column-message', 'Message'),
|
||||
cell: ({ row: { original: job } }: JobCell) => <span>{job.status?.message}</span>,
|
||||
},
|
||||
];
|
||||
@@ -91,11 +92,10 @@ function ExpandedRow({ row }: ExpandedRowProps) {
|
||||
const hasSpec = Boolean(row.spec);
|
||||
|
||||
if (!hasSummary && !hasErrors && !hasSpec) {
|
||||
console.log('no summary, errors, or spec', row);
|
||||
return null;
|
||||
}
|
||||
|
||||
// the action is already showin
|
||||
// the action is already showing
|
||||
const data = useMemo(() => {
|
||||
const v: TraceKeyValuePair[] = [];
|
||||
const action = row.spec?.action;
|
||||
@@ -118,7 +118,7 @@ function ExpandedRow({ row }: ExpandedRowProps) {
|
||||
{hasSpec && (
|
||||
<Stack direction="column">
|
||||
<Text variant="body" color="secondary">
|
||||
Job Specification
|
||||
<Trans i18nKey="provisioning.expanded-row.job-specification">Job Specification</Trans>
|
||||
</Text>
|
||||
<KeyValuesTable data={data} />
|
||||
</Stack>
|
||||
@@ -128,7 +128,7 @@ function ExpandedRow({ row }: ExpandedRowProps) {
|
||||
{row.status?.errors?.map(
|
||||
(error, index) =>
|
||||
error.trim() && (
|
||||
<Alert key={index} severity="error" title="Error">
|
||||
<Alert key={index} severity="error" title={t('provisioning.expanded-row.title-error', 'Error')}>
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Icon name="exclamation-circle" size="sm" />
|
||||
{error}
|
||||
@@ -141,7 +141,7 @@ function ExpandedRow({ row }: ExpandedRowProps) {
|
||||
{hasSummary && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text variant="body" color="secondary">
|
||||
Summary
|
||||
<Trans i18nKey="provisioning.expanded-row.summary">Summary</Trans>
|
||||
</Text>
|
||||
<JobSummary summary={row.status!.summary!} />
|
||||
</Stack>
|
||||
@@ -154,14 +154,19 @@ function ExpandedRow({ row }: ExpandedRowProps) {
|
||||
function EmptyState() {
|
||||
return (
|
||||
<Stack direction={'column'} alignItems={'center'}>
|
||||
<Text color="secondary">No jobs...</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.empty-state.no-jobs">No jobs...</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorLoading(typ: string, error: any) {
|
||||
function ErrorLoading(typ: string, error: string) {
|
||||
return (
|
||||
<Alert title={`Error loading ${typ}`} severity="error">
|
||||
<Alert
|
||||
title={t('provisioning.recent-jobs.error-loading', 'Error loading {{type}}', { type: typ })}
|
||||
severity="error"
|
||||
>
|
||||
<pre>{JSON.stringify(error)}</pre>
|
||||
</Alert>
|
||||
);
|
||||
@@ -189,7 +194,7 @@ export function RecentJobs({ repo }: Props) {
|
||||
if (activeQuery.isLoading || historicQuery.isLoading) {
|
||||
description = Loading();
|
||||
} else if (activeQuery.isError) {
|
||||
description = ErrorLoading('active jobs', activeQuery.error);
|
||||
description = ErrorLoading(t('provisioning.recent-jobs.active-jobs', 'active jobs'), activeQuery.error);
|
||||
// TODO: Figure out what to do if historic fails. Maybe a separate card?
|
||||
} else if (!jobs?.length) {
|
||||
description = <EmptyState />;
|
||||
@@ -207,7 +212,9 @@ export function RecentJobs({ repo }: Props) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Heading>Jobs</Card.Heading>
|
||||
<Card.Heading>
|
||||
<Trans i18nKey="provisioning.recent-jobs.jobs">Jobs</Trans>
|
||||
</Card.Heading>
|
||||
<Card.Description>{description}</Card.Description>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Button, Spinner } from '@grafana/ui';
|
||||
import { Repository, useCreateRepositoryTestMutation } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
interface Props {
|
||||
repository: Repository;
|
||||
@@ -42,7 +43,7 @@ export function CheckRepository({ repository }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Button icon="check-circle" variant={'secondary'} disabled={testQuery.isLoading || !name} onClick={onClick}>
|
||||
Check
|
||||
<Trans i18nKey="provisioning.check-repository.check">Check</Trans>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { ConfirmModal, IconButton } from '@grafana/ui';
|
||||
import { useDeleteRepositoryMutation } from 'app/api/clients/provisioning';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
const appEvents = getAppEvents();
|
||||
|
||||
@@ -22,7 +23,12 @@ export function DeleteRepositoryButton({ name, redirectTo }: Props) {
|
||||
if (request.isSuccess) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Repository settings queued for deletion'],
|
||||
payload: [
|
||||
t(
|
||||
'provisioning.delete-repository-button.success-repository-deleted',
|
||||
'Repository settings queued for deletion'
|
||||
),
|
||||
],
|
||||
});
|
||||
setShowModal(false);
|
||||
if (redirectTo) {
|
||||
@@ -39,7 +45,7 @@ export function DeleteRepositoryButton({ name, redirectTo }: Props) {
|
||||
<>
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
tooltip="Delete this repository"
|
||||
tooltip={t('provisioning.delete-repository-button.tooltip-delete-this-repository', 'Delete this repository')}
|
||||
disabled={request.isLoading}
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
@@ -47,9 +53,12 @@ export function DeleteRepositoryButton({ name, redirectTo }: Props) {
|
||||
/>
|
||||
<ConfirmModal
|
||||
isOpen={showModal}
|
||||
title={'Delete repository config'}
|
||||
body={'Are you sure you want to delete the repository config?'}
|
||||
confirmText={'Delete'}
|
||||
title={t('provisioning.delete-repository-button.title-delete-repository', 'Delete repository config')}
|
||||
body={t(
|
||||
'provisioning.delete-repository-button.confirm-delete-repository',
|
||||
'Are you sure you want to delete the repository config?'
|
||||
)}
|
||||
confirmText={t('provisioning.delete-repository-button.button-delete', 'Delete')}
|
||||
onConfirm={onConfirm}
|
||||
onDismiss={() => setShowModal(false)}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { EmptyState, Text, TextLink } from '@grafana/ui';
|
||||
import { useGetRepositoryQuery } from 'app/api/clients/provisioning';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { ConfigForm } from '../Config/ConfigForm';
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
@@ -20,8 +21,14 @@ export default function EditRepositoryPage() {
|
||||
<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>
|
||||
<Text element={'p'}>
|
||||
<Trans i18nKey="provisioning.edit-repository-page.repository-config-exists-configuration">
|
||||
Make sure the repository config exists in the configuration file.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TextLink href={PROVISIONING_URL}>
|
||||
<Trans i18nKey="provisioning.edit-repository-page.back-to-repositories">Back to repositories</Trans>
|
||||
</TextLink>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<ConfigForm data={query.data} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, LinkButton, Stack } from '@grafana/ui';
|
||||
import { Repository } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { StatusBadge } from '../Shared/StatusBadge';
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
@@ -21,12 +22,12 @@ export function RepositoryActions({ repository }: RepositoryActionsProps) {
|
||||
<StatusBadge repo={repository} />
|
||||
{repoHref && (
|
||||
<Button variant="secondary" icon="github" onClick={() => window.open(repoHref, '_blank')}>
|
||||
Source Code
|
||||
<Trans i18nKey="provisioning.repository-actions.source-code">Source Code</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<SyncRepository repository={repository} />
|
||||
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`}>
|
||||
Settings
|
||||
<Trans i18nKey="provisioning.repository-actions.settings">Settings</Trans>
|
||||
</LinkButton>
|
||||
<DeleteRepositoryButton name={name} redirectTo={PROVISIONING_URL} />
|
||||
</Stack>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactNode } from 'react';
|
||||
|
||||
import { IconName, Stack, Text, TextLink, Icon, Card, LinkButton } from '@grafana/ui';
|
||||
import { Repository, ResourceCount } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { StatusBadge } from '../Shared/StatusBadge';
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
@@ -35,7 +36,7 @@ export function RepositoryCard({ repository }: Props) {
|
||||
meta.push(
|
||||
<Stack gap={1} direction="row" alignItems="center">
|
||||
<TextLink key="webhook" href={webhookUrl}>
|
||||
Webhook
|
||||
<Trans i18nKey="provisioning.repository-card.get-repository-meta.webhook">Webhook</Trans>
|
||||
</TextLink>
|
||||
<Icon name="check" className="text-success" />
|
||||
</Stack>
|
||||
@@ -98,11 +99,11 @@ export function RepositoryCard({ repository }: Props) {
|
||||
<Card.Actions>
|
||||
<Stack gap={1} direction="row">
|
||||
<LinkButton icon="eye" href={`${PROVISIONING_URL}/${name}`} variant="primary" size="md">
|
||||
View
|
||||
<Trans i18nKey="provisioning.repository-card.view">View</Trans>
|
||||
</LinkButton>
|
||||
<SyncRepository repository={repository} />
|
||||
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`} size="md">
|
||||
Settings
|
||||
<Trans i18nKey="provisioning.repository-card.settings">Settings</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</Card.Actions>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Stack, Alert, Text } from '@grafana/ui';
|
||||
import { HealthStatus } from 'app/api/clients/provisioning';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
interface Props {
|
||||
health: HealthStatus;
|
||||
@@ -9,14 +10,24 @@ export function RepositoryHealth({ health }: Props) {
|
||||
return (
|
||||
<Stack gap={2} direction="column" alignItems="flex-start">
|
||||
{health.healthy ? (
|
||||
<Alert title="Repository is healthy" severity="success" style={{ width: '100%' }}>
|
||||
No errors found
|
||||
<Alert
|
||||
title={t('provisioning.repository-health.title-repository-is-healthy', 'Repository is healthy')}
|
||||
severity="success"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Trans i18nKey="provisioning.repository-health.no-errors-found">No errors found</Trans>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert title="Repository is unhealthy" severity="warning" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
title={t('provisioning.repository-health.title-repository-is-unhealthy', 'Repository is unhealthy')}
|
||||
severity="warning"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{health.message && health.message.length > 0 && (
|
||||
<>
|
||||
<Text>Details:</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="provisioning.repository-health.details">Details:</Trans>
|
||||
</Text>
|
||||
<ul>
|
||||
{health.message.map((message) => (
|
||||
<li key={message}>{message}</li>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Repository, ResourceCount } from 'app/api/clients/provisioning';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { RecentJobs } from '../Job/RecentJobs';
|
||||
import { formatTimestamp } from '../utils/time';
|
||||
@@ -56,7 +57,9 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
<Grid columns={3} gap={2}>
|
||||
<div className={styles.cardContainer}>
|
||||
<Card className={styles.card}>
|
||||
<Card.Heading>Resources</Card.Heading>
|
||||
<Card.Heading>
|
||||
<Trans i18nKey="provisioning.repository-overview.resources">Resources</Trans>
|
||||
</Card.Heading>
|
||||
<Card.Description>
|
||||
{repo.status?.stats ? (
|
||||
<InteractiveTable
|
||||
@@ -68,7 +71,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
</Card.Description>
|
||||
<Card.Actions className={styles.actions}>
|
||||
<LinkButton fill="outline" size="md" href={getFolderURL(repo)} icon="folder-open">
|
||||
View Folder
|
||||
<Trans i18nKey="provisioning.repository-overview.view-folder">View Folder</Trans>
|
||||
</LinkButton>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
@@ -76,19 +79,25 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
{repo.status?.health && (
|
||||
<div className={styles.cardContainer}>
|
||||
<Card className={styles.card}>
|
||||
<Card.Heading>Health</Card.Heading>
|
||||
<Card.Heading>
|
||||
<Trans i18nKey="provisioning.repository-overview.health">Health</Trans>
|
||||
</Card.Heading>
|
||||
<Card.Description>
|
||||
<RepositoryHealth health={repo.status?.health} />
|
||||
<Grid columns={12} gap={1} alignItems="baseline">
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Status:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.status">Status:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{status?.health?.healthy ? 'Healthy' : 'Unhealthy'}</Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Checked:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.checked">Checked:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{formatTimestamp(status?.health?.checked)}</Text>
|
||||
@@ -97,7 +106,9 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
{!!status?.health?.message?.length && (
|
||||
<>
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Messages:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.messages">Messages:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Stack gap={1}>
|
||||
@@ -120,39 +131,51 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
)}
|
||||
<div className={styles.cardContainer}>
|
||||
<Card className={styles.card}>
|
||||
<Card.Heading>Pull status</Card.Heading>
|
||||
<Card.Heading>
|
||||
<Trans i18nKey="provisioning.repository-overview.pull-status">Pull status</Trans>
|
||||
</Card.Heading>
|
||||
<Card.Description>
|
||||
<Grid columns={12} gap={1} alignItems="baseline">
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Status:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.status">Status:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{status?.sync.state ?? 'N/A'}</Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Job ID:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.job-id">Job ID:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{status?.sync.job ?? 'N/A'}</Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Last Ref:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.last-ref">Last Ref:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{status?.sync.lastRef ? status.sync.lastRef.substring(0, 7) : 'N/A'}</Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Started:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.started">Started:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{formatTimestamp(status?.sync.started)}</Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Finished:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.finished">Finished:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Text variant="body">{formatTimestamp(status?.sync.finished)}</Text>
|
||||
@@ -161,7 +184,9 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
{!!status?.sync?.message?.length && (
|
||||
<>
|
||||
<div className={styles.labelColumn}>
|
||||
<Text color="secondary">Messages:</Text>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="provisioning.repository-overview.messages">Messages:</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.valueColumn}>
|
||||
<Stack gap={1}>
|
||||
@@ -180,7 +205,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
|
||||
<SyncRepository repository={repo} />
|
||||
{webhookURL && (
|
||||
<TextLink external href={webhookURL} icon="link">
|
||||
Webhook
|
||||
<Trans i18nKey="provisioning.repository-overview.webhook">Webhook</Trans>
|
||||
</TextLink>
|
||||
)}
|
||||
</Card.Actions>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { CellProps, Column, FilterInput, InteractiveTable, Link, LinkButton, Spinner, Stack } from '@grafana/ui';
|
||||
import { Repository, ResourceListItem, useGetRepositoryResourcesQuery } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
|
||||
@@ -21,6 +22,7 @@ export function RepositoryResources({ repo }: RepoProps) {
|
||||
const data = (query.data?.items ?? []).filter((Resource) =>
|
||||
Resource.path.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const columns: Array<Column<ResourceListItem>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -86,9 +88,19 @@ export function RepositoryResources({ repo }: RepoProps) {
|
||||
const { resource, name, path } = original;
|
||||
return (
|
||||
<Stack>
|
||||
{resource === 'dashboards' && <LinkButton href={`/d/${name}`}>View</LinkButton>}
|
||||
{resource === 'folders' && <LinkButton href={`/dashboards/f/${name}`}>View</LinkButton>}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${repo.metadata?.name}/history/${path}`}>History</LinkButton>
|
||||
{resource === 'dashboards' && (
|
||||
<LinkButton href={`/d/${name}`}>
|
||||
<Trans i18nKey="provisioning.repository-resources.columns.view-dashboard">View</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
{resource === 'folders' && (
|
||||
<LinkButton href={`/dashboards/f/${name}`}>
|
||||
<Trans i18nKey="provisioning.repository-resources.columns.view-folder">View</Trans>
|
||||
</LinkButton>
|
||||
)}
|
||||
<LinkButton href={`${PROVISIONING_URL}/${repo.metadata?.name}/history/${path}`}>
|
||||
<Trans i18nKey="provisioning.repository-resources.columns.history">History</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
@@ -108,7 +120,12 @@ export function RepositoryResources({ repo }: RepoProps) {
|
||||
return (
|
||||
<Stack grow={1} direction={'column'} gap={2}>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" autoFocus={true} value={searchQuery} onChange={setSearchQuery} />
|
||||
<FilterInput
|
||||
placeholder={t('provisioning.repository-resources.placeholder-search', 'Search')}
|
||||
autoFocus={true}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
/>
|
||||
</Stack>
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
@@ -6,6 +7,7 @@ import { Alert, EmptyState, Spinner, Tab, TabContent, TabsBar, Text, TextLink }
|
||||
import { useGetFrontendSettingsQuery, useListRepositoryQuery } from 'app/api/clients/provisioning';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { isNotFoundError } from 'app/features/alerting/unified/api/util';
|
||||
|
||||
import { FilesView } from '../File/FilesView';
|
||||
@@ -21,12 +23,6 @@ enum TabSelection {
|
||||
Files = 'files',
|
||||
}
|
||||
|
||||
const tabInfo: SelectableValue<TabSelection> = [
|
||||
{ value: TabSelection.Overview, label: 'Overview', title: 'Repository overview' },
|
||||
{ value: TabSelection.Resources, label: 'Resources', title: 'Resources saved in grafana database' },
|
||||
{ value: TabSelection.Files, label: 'Files', title: 'The raw file list from the repository' },
|
||||
];
|
||||
|
||||
export default function RepositoryStatusPage() {
|
||||
const { name = '' } = useParams();
|
||||
|
||||
@@ -42,25 +38,60 @@ export default function RepositoryStatusPage() {
|
||||
|
||||
const notFound = query.isError && isNotFoundError(query.error);
|
||||
|
||||
const tabInfo = useMemo<SelectableValue<TabSelection>>(
|
||||
() => [
|
||||
{
|
||||
value: TabSelection.Overview,
|
||||
label: t('provisioning.repository-status-page.tab-overview', 'Overview'),
|
||||
title: t('provisioning.repository-status-page.tab-overview-title', 'Repository overview'),
|
||||
},
|
||||
{
|
||||
value: TabSelection.Resources,
|
||||
label: t('provisioning.repository-status-page.tab-resources', 'Resources'),
|
||||
title: t('provisioning.repository-status-page.tab-resources-title', 'Resources saved in grafana database'),
|
||||
},
|
||||
{
|
||||
value: TabSelection.Files,
|
||||
label: t('provisioning.repository-status-page.tab-files', 'Files'),
|
||||
title: t('provisioning.repository-status-page.tab-files-title', 'The raw file list from the repository'),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="provisioning"
|
||||
pageNav={{
|
||||
text: data?.spec?.title ?? 'Repository Status',
|
||||
text: data?.spec?.title ?? t('provisioning.repository-status-page.title', 'Repository Status'),
|
||||
subTitle: data?.spec?.description,
|
||||
}}
|
||||
actions={data && <RepositoryActions repository={data} />}
|
||||
>
|
||||
<Page.Contents isLoading={query.isLoading}>
|
||||
{settings.data?.legacyStorage && (
|
||||
<Alert title="Legacy Storage" severity="error">
|
||||
Instance is not yet running unified storage -- requires migration wizard
|
||||
<Alert
|
||||
title={t('provisioning.repository-status-page.title-legacy-storage', 'Legacy Storage')}
|
||||
severity="error"
|
||||
>
|
||||
<Trans i18nKey="provisioning.repository-status-page.legacy-storage-message">
|
||||
Instance is not yet running unified storage -- requires migration wizard
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{notFound ? (
|
||||
<EmptyState message={`Repository 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
|
||||
message={t('provisioning.repository-status-page.not-found-message', 'Repository not found')}
|
||||
variant="not-found"
|
||||
>
|
||||
<Text element={'p'}>
|
||||
<Trans i18nKey="provisioning.repository-status-page.repository-config-exists-configuration">
|
||||
Make sure the repository config exists in the configuration file.
|
||||
</Trans>
|
||||
</Text>
|
||||
<TextLink href={PROVISIONING_URL}>
|
||||
<Trans i18nKey="provisioning.repository-status-page.back-to-repositories">Back to repositories</Trans>
|
||||
</TextLink>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<>
|
||||
@@ -79,8 +110,14 @@ export default function RepositoryStatusPage() {
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
{data?.metadata?.deletionTimestamp && (
|
||||
<Alert title="Queued for deletion" severity="warning">
|
||||
<Spinner /> Cleaning up repository resources
|
||||
<Alert
|
||||
title={t('provisioning.repository-status-page.title-queued-for-deletion', 'Queued for deletion')}
|
||||
severity="warning"
|
||||
>
|
||||
<Spinner />{' '}
|
||||
<Trans i18nKey="provisioning.repository-status-page.cleaning-up-resources">
|
||||
Cleaning up repository resources
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{tab === TabSelection.Overview && <RepositoryOverview repo={data} />}
|
||||
@@ -89,7 +126,9 @@ export default function RepositoryStatusPage() {
|
||||
</TabContent>
|
||||
</>
|
||||
) : (
|
||||
<div>not found</div>
|
||||
<div>
|
||||
<Trans i18nKey="provisioning.repository-status-page.not-found">not found</Trans>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Button, ConfirmModal } from '@grafana/ui';
|
||||
import { Repository, useCreateRepositorySyncMutation } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
|
||||
@@ -23,12 +24,15 @@ export function SyncRepository({ repository }: Props) {
|
||||
if (syncQuery.isSuccess) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Pull started'],
|
||||
payload: [t('provisioning.sync-repository.success-pull-started', 'Pull started')],
|
||||
});
|
||||
} else if (syncQuery.isError) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: ['Error pulling resources', syncQuery.error],
|
||||
payload: [
|
||||
t('provisioning.sync-repository.error-pulling-resources', 'Error pulling resources'),
|
||||
syncQuery.error,
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [syncQuery.error, syncQuery.isError, syncQuery.isSuccess]);
|
||||
@@ -48,18 +52,22 @@ export function SyncRepository({ repository }: Props) {
|
||||
<Button
|
||||
icon="cloud-download"
|
||||
variant={'secondary'}
|
||||
tooltip={isHealthy ? undefined : 'Unable to pull an unhealthy repository'}
|
||||
tooltip={
|
||||
isHealthy
|
||||
? undefined
|
||||
: t('provisioning.sync-repository.tooltip-unhealthy-repository', 'Unable to pull an unhealthy repository')
|
||||
}
|
||||
disabled={syncQuery.isLoading || !name || !isHealthy}
|
||||
onClick={onClick}
|
||||
>
|
||||
Pull
|
||||
<Trans i18nKey="provisioning.sync-repository.pull">Pull</Trans>
|
||||
</Button>
|
||||
{!repository.spec?.sync.enabled && (
|
||||
<ConfirmModal
|
||||
isOpen={isModalOpen}
|
||||
title={'Pull is not enabled'}
|
||||
body={`Edit the configuration`}
|
||||
confirmText={'Edit'}
|
||||
title={t('provisioning.sync-repository.title-pull-not-enabled', 'Pull is not enabled')}
|
||||
body={t('provisioning.sync-repository.body-edit-configuration', 'Edit the configuration')}
|
||||
confirmText={t('provisioning.sync-repository.button-edit', 'Edit')}
|
||||
onConfirm={() => navigate(`${PROVISIONING_URL}/${name}/edit`)}
|
||||
onDismiss={() => setIsModalOpen(false)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
export function BranchValidationError() {
|
||||
return (
|
||||
<>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.invalid-branch-name">Invalid branch name.</Trans>
|
||||
<ul style={{ padding: '0 20px' }}>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.cannot-start-with">
|
||||
It cannot start with '/' or end with '/', '.', or whitespace.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.it-cannot-contain-or">
|
||||
It cannot contain '//' or '..'.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.cannot-contain-invalid-characters">
|
||||
It cannot contain invalid characters: '~', '^', ':', '?', '*', '[', '\\', or ']'.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans i18nKey="dashboard-scene.branch-validation-error.least-valid-character">
|
||||
It must have at least one valid character.
|
||||
</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LinkButton } from '@grafana/ui';
|
||||
import { Repository } from 'app/api/clients/provisioning';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
|
||||
import { CONNECT_URL } from '../constants';
|
||||
import { checkSyncSettings } from '../utils/checkSyncSettings';
|
||||
@@ -22,16 +23,24 @@ export function ConnectRepositoryButton({ items }: Props) {
|
||||
variant="primary"
|
||||
icon="plus"
|
||||
disabled={true}
|
||||
tooltip={`Max repositories already created (${state.repoCount})`}
|
||||
tooltip={t(
|
||||
'provisioning.connect-repository-button.tooltip-max-repos',
|
||||
'Max repositories already created ({{count}})',
|
||||
{ count: state.repoCount }
|
||||
)}
|
||||
>
|
||||
Maximum repositories exist ({state.repoCount})
|
||||
<Trans
|
||||
i18nKey="provisioning.connect-repository-button.max-repositories-exist"
|
||||
values={{ count: state.repoCount }}
|
||||
defaults={'Maximum repositories exist ({{count}})'}
|
||||
/>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkButton href={CONNECT_URL} variant="primary" icon="plus">
|
||||
Connect to repository
|
||||
<Trans i18nKey="provisioning.connect-repository-button.connect-to-repository">Connect to repository</Trans>
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
|
||||
import { EmptySearchResult, FilterInput, Stack } from '@grafana/ui';
|
||||
import { Repository } from 'app/api/clients/provisioning';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { RepositoryCard } from '../Repository/RepositoryCard';
|
||||
|
||||
@@ -18,14 +19,22 @@ export function FolderRepositoryList({ items }: Props) {
|
||||
return (
|
||||
<Stack direction={'column'} gap={3}>
|
||||
<Stack gap={2}>
|
||||
<FilterInput placeholder="Search" value={query} onChange={setQuery} />
|
||||
<FilterInput
|
||||
placeholder={t('provisioning.folder-repository-list.placeholder-search', 'Search')}
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
/>
|
||||
<ConnectRepositoryButton items={items} />
|
||||
</Stack>
|
||||
<Stack direction={'column'}>
|
||||
{filteredItems.length ? (
|
||||
filteredItems.map((item) => <RepositoryCard key={item.metadata?.name} repository={item} />)
|
||||
) : (
|
||||
<EmptySearchResult>No results matching your query </EmptySearchResult>
|
||||
<EmptySearchResult>
|
||||
<Trans i18nKey="provisioning.folder-repository-list.no-results-matching-your-query">
|
||||
No results matching your query
|
||||
</Trans>
|
||||
</EmptySearchResult>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { TextLink, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
export function TokenPermissionsInfo() {
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -9,34 +10,56 @@ export function TokenPermissionsInfo() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
Go to{' '}
|
||||
<TextLink external href="https://github.com/settings/personal-access-tokens/new">
|
||||
GitHub Personal Access Tokens
|
||||
</TextLink>
|
||||
. Make sure to include these permissions under <b>Repository</b>:
|
||||
<Trans i18nKey="provisioning.token-permissions-info.github-instructions">
|
||||
Go to{' '}
|
||||
<TextLink external href="https://github.com/settings/personal-access-tokens/new">
|
||||
GitHub Personal Access Tokens
|
||||
</TextLink>
|
||||
. Make sure to include these permissions under <b>Repository</b>:
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<table className={styles.permissionTable}>
|
||||
<tbody>
|
||||
<tr className={styles.headerSeparator}>
|
||||
<th>Permission</th>
|
||||
<th>Access</th>
|
||||
<th>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.permission">Permission</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.access">Access</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Contents</td>
|
||||
<td>Read and write</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.contents">Contents</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.read-and-write">Read and write</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metadata</td>
|
||||
<td>Read-only</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.metadata">Metadata</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.readonly">Read-only</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pull requests</td>
|
||||
<td>Read and write</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.pull-requests">Pull requests</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.read-and-write">Read and write</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Webhooks</td>
|
||||
<td>Read and write</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.webhooks">Webhooks</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Trans i18nKey="provisioning.token-permissions-info.read-and-write">Read and write</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@grafana/ui';
|
||||
import { RepositoryViewList, useGetRepositoryFilesQuery, useGetResourceStatsQuery } from 'app/api/clients/provisioning';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { StepStatus } from '../hooks/useStepStatus';
|
||||
|
||||
@@ -76,7 +77,9 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props)
|
||||
if (resourceStats.isLoading || filesQuery.isLoading) {
|
||||
return (
|
||||
<Box padding={4}>
|
||||
<LoadingPlaceholder text="Loading resource information..." />
|
||||
<LoadingPlaceholder
|
||||
text={t('provisioning.bootstrap-step.text-loading-resource-information', 'Loading resource information...')}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -88,19 +91,27 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props)
|
||||
<Stack direction="row" gap={4} alignItems="flex-start" justifyContent="center">
|
||||
<Stack direction="column" gap={1} alignItems="center">
|
||||
<Text variant="h4" color="secondary">
|
||||
Grafana
|
||||
<Trans i18nKey="provisioning.bootstrap-step.grafana">Grafana</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={2}>
|
||||
<Text variant="h3">
|
||||
<Text variant="h3">{state.resourceCount > 0 ? state.resourceCountString : 'Empty'}</Text>
|
||||
<Text variant="h3">
|
||||
{state.resourceCount > 0
|
||||
? state.resourceCountString
|
||||
: t('provisioning.bootstrap-step.empty', 'Empty')}
|
||||
</Text>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="column" gap={1} alignItems="center">
|
||||
<Text variant="h4" color="secondary">
|
||||
Repository
|
||||
<Trans i18nKey="provisioning.bootstrap-step.repository">Repository</Trans>
|
||||
</Text>
|
||||
<Text variant="h3">
|
||||
{state.fileCount > 0
|
||||
? t('provisioning.bootstrap-step.files-count', '{{count}} files', { count: state.fileCount })
|
||||
: t('provisioning.bootstrap-step.empty', 'Empty')}
|
||||
</Text>
|
||||
<Text variant="h3">{state.fileCount > 0 ? `${state.fileCount} files` : 'Empty'}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -131,23 +142,36 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props)
|
||||
{selectedOption?.operation === 'migrate' && (
|
||||
<>
|
||||
{Boolean(state.resourceCount) && (
|
||||
<Alert severity="info" title="Note">
|
||||
Dashboards will be unavailable while running this process
|
||||
<Alert severity="info" title={t('provisioning.bootstrap-step.title-note', 'Note')}>
|
||||
<Trans i18nKey="provisioning.bootstrap-step.dashboards-unavailable-while-running-process">
|
||||
Dashboards will be unavailable while running this process
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{Boolean(state.fileCount) && Boolean(state.resourceCount) && (
|
||||
<Alert title="Files exist in the target" severity="info">
|
||||
The {state.resourceCount} resources in grafana will be added to the repository. Grafana will then
|
||||
include both the current resources and anything from the repository when done.
|
||||
<Alert
|
||||
title={t('provisioning.bootstrap-step.title-files-exist-in-the-target', 'Files exist in the target')}
|
||||
severity="info"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="provisioning.bootstrap-step.resources-will-be-added"
|
||||
defaults="The {{count}} resources in grafana will be added to the repository. Grafana will then include both the current resources and anything from the repository when done."
|
||||
values={{ count: state.resourceCount }}
|
||||
/>
|
||||
</Alert>
|
||||
)}
|
||||
<FieldSet label="Migrate options">
|
||||
<FieldSet label={t('provisioning.bootstrap-step.label-migrate-options', 'Migrate options')}>
|
||||
<Stack direction="column" gap={2}>
|
||||
<Stack direction="row" gap={2} alignItems="center">
|
||||
<Switch {...register('migrate.identifier')} defaultChecked={true} />
|
||||
<Text>Include identifiers</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="provisioning.bootstrap-step.include-identifiers">Include identifiers</Trans>
|
||||
</Text>
|
||||
<Tooltip
|
||||
content="Include unique identifiers for each dashboard to maintain references"
|
||||
content={t(
|
||||
'provisioning.bootstrap-step.tooltip-include-identifiers',
|
||||
'Include unique identifiers for each dashboard to maintain references'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon name="info-circle" />
|
||||
@@ -156,8 +180,16 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props)
|
||||
{repoType === 'github' && settingsData?.legacyStorage && (
|
||||
<Stack direction="row" gap={2} alignItems="center">
|
||||
<Switch {...register('migrate.history')} defaultChecked={true} />
|
||||
<Text>Include history</Text>
|
||||
<Tooltip content="Include complete dashboard version history" placement="top">
|
||||
<Text>
|
||||
<Trans i18nKey="provisioning.bootstrap-step.include-history">Include history</Trans>
|
||||
</Text>
|
||||
<Tooltip
|
||||
content={t(
|
||||
'provisioning.bootstrap-step.tooltip-include-history',
|
||||
'Include complete dashboard version history'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
@@ -170,14 +202,22 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props)
|
||||
{/* Only show title field if folder sync */}
|
||||
{selectedTarget === 'folder' && (
|
||||
<Field
|
||||
label="Display name"
|
||||
description="Add a clear name for this repository connection"
|
||||
label={t('provisioning.bootstrap-step.label-display-name', 'Display name')}
|
||||
description={t(
|
||||
'provisioning.bootstrap-step.description-clear-repository-connection',
|
||||
'Add a clear name for this repository connection'
|
||||
)}
|
||||
error={errors.repository?.title?.message}
|
||||
invalid={!!errors.repository?.title}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.title', { required: 'This field is required.' })}
|
||||
placeholder="My repository connection"
|
||||
{...register('repository.title', {
|
||||
required: t('provisioning.bootstrap-step.error-field-required', 'This field is required.'),
|
||||
})}
|
||||
placeholder={t(
|
||||
'provisioning.bootstrap-step.placeholder-my-repository-connection',
|
||||
'My repository connection'
|
||||
)}
|
||||
// Auto-focus the title field if it's the only available option
|
||||
autoFocus={state.actions.length === 1 && state.actions[0].target === 'folder'}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Combobox, ComboboxOption, Field, Input, SecretInput, Stack } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { getWorkflowOptions } from '../Config/ConfigForm';
|
||||
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
|
||||
|
||||
import { WizardFormData } from './types';
|
||||
|
||||
const typeOptions: Array<ComboboxOption<'github' | 'local'>> = [
|
||||
{ label: 'GitHub', value: 'github' },
|
||||
{ label: 'Local', value: 'local' },
|
||||
];
|
||||
|
||||
export function ConnectStep() {
|
||||
const {
|
||||
register,
|
||||
@@ -25,11 +21,26 @@ export function ConnectStep() {
|
||||
const type = watch('repository.type');
|
||||
const [tokenConfigured, setTokenConfigured] = useState(false);
|
||||
|
||||
const typeOptions = useMemo<Array<ComboboxOption<'github' | 'local'>>>(
|
||||
() => [
|
||||
{ label: t('provisioning.connect-step.storage-type-github', 'GitHub'), value: 'github' },
|
||||
{ label: t('provisioning.connect-step.storage-type-local', 'Local'), value: 'local' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const isGithub = type === 'github';
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Field label="Storage type" required description="Choose the type of storage for your resources">
|
||||
<Field
|
||||
label={t('provisioning.connect-step.label-storage-type', 'Storage type')}
|
||||
required
|
||||
description={t(
|
||||
'provisioning.connect-step.description-choose-storage-resources',
|
||||
'Choose the type of storage for your resources'
|
||||
)}
|
||||
>
|
||||
<Combobox
|
||||
options={typeOptions}
|
||||
value={type}
|
||||
@@ -48,22 +59,28 @@ export function ConnectStep() {
|
||||
<>
|
||||
<TokenPermissionsInfo />
|
||||
<Field
|
||||
label={'Enter your access token'}
|
||||
label={t('provisioning.connect-step.label-access-token', 'Enter your access token')}
|
||||
required
|
||||
description="Paste your GitHub personal access token"
|
||||
description={t(
|
||||
'provisioning.connect-step.description-paste-your-git-hub-personal-access-token',
|
||||
'Paste your GitHub personal access token'
|
||||
)}
|
||||
error={errors.repository?.token?.message}
|
||||
invalid={!!errors.repository?.token}
|
||||
>
|
||||
<Controller
|
||||
name={'repository.token'}
|
||||
control={control}
|
||||
rules={{ required: 'This field is required.' }}
|
||||
rules={{ required: t('provisioning.connect-step.error-field-required', 'This field is required.') }}
|
||||
render={({ field: { ref, ...field } }) => {
|
||||
return (
|
||||
<SecretInput
|
||||
{...field}
|
||||
id={'token'}
|
||||
placeholder={'github_pat_yourTokenHere1234567890abcdEFGHijklMNOP'}
|
||||
placeholder={t(
|
||||
'provisioning.connect-step.placeholder-github-token',
|
||||
'github_pat_yourTokenHere1234567890abcdEFGHijklMNOP'
|
||||
)}
|
||||
isConfigured={tokenConfigured}
|
||||
onReset={() => {
|
||||
setValue('repository.token', '');
|
||||
@@ -76,45 +93,70 @@ export function ConnectStep() {
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={'Enter your Repository URL'}
|
||||
label={t('provisioning.connect-step.label-repository-url', 'Enter your Repository URL')}
|
||||
error={errors.repository?.url?.message}
|
||||
invalid={!!errors.repository?.url}
|
||||
description={'Paste the URL of your GitHub repository'}
|
||||
description={t(
|
||||
'provisioning.connect-step.description-repository-url',
|
||||
'Paste the URL of your GitHub repository'
|
||||
)}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
{...register('repository.url', {
|
||||
required: 'This field is required.',
|
||||
required: t('provisioning.connect-step.error-field-required', 'This field is required.'),
|
||||
pattern: {
|
||||
// TODO: The regex is not correct when we support GHES.
|
||||
value: /^(?:https:\/\/github\.com\/)?[^/]+\/[^/]+$/,
|
||||
message: 'Please enter a valid GitHub repository URL',
|
||||
message: t(
|
||||
'provisioning.connect-step.error-invalid-github-url',
|
||||
'Please enter a valid GitHub repository URL'
|
||||
),
|
||||
},
|
||||
})}
|
||||
placeholder={'https://github.com/username/repo'}
|
||||
placeholder={t('provisioning.connect-step.placeholder-github-url', 'https://github.com/username/repo')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={'Branch'} error={errors.repository?.branch?.message} invalid={!!errors.repository?.branch}>
|
||||
<Input {...register('repository.branch')} placeholder={'main'} />
|
||||
<Field
|
||||
label={t('provisioning.connect-step.label-branch', 'Branch')}
|
||||
error={errors.repository?.branch?.message}
|
||||
invalid={!!errors.repository?.branch}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.branch')}
|
||||
placeholder={t('provisioning.connect-step.placeholder-branch', 'main')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={'Path'}
|
||||
label={t('provisioning.connect-step.label-path', 'Path')}
|
||||
error={errors.repository?.path?.message}
|
||||
invalid={!!errors.repository?.path}
|
||||
description={'Path to a subdirectory in the Git repository'}
|
||||
description={t(
|
||||
'provisioning.connect-step.description-github-path',
|
||||
'Path to a subdirectory in the Git repository'
|
||||
)}
|
||||
>
|
||||
<Input {...register('repository.path')} placeholder={'grafana/'} />
|
||||
<Input
|
||||
{...register('repository.path')}
|
||||
placeholder={t('provisioning.connect-step.placeholder-github-path', 'grafana/')}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === 'local' && (
|
||||
<Field label={'Local path'} error={errors.repository?.path?.message} invalid={!!errors.repository?.path}>
|
||||
<Field
|
||||
label={t('provisioning.connect-step.label-local-path', 'Local path')}
|
||||
error={errors.repository?.path?.message}
|
||||
invalid={!!errors.repository?.path}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.path', { required: 'This field is required.' })}
|
||||
placeholder={'/path/to/repo'}
|
||||
{...register('repository.path', {
|
||||
required: t('provisioning.connect-step.error-field-required', 'This field is required.'),
|
||||
})}
|
||||
placeholder={t('provisioning.connect-step.placeholder-local-path', '/path/to/repo')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Field, Input, MultiCombobox, Stack, Switch, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { getWorkflowOptions } from '../Config/ConfigForm';
|
||||
import { checkPublicAccess, checkImageRenderer } from '../GettingStarted/features';
|
||||
@@ -42,21 +43,30 @@ export function FinishStep() {
|
||||
<Stack direction="column">
|
||||
{isGithub && (
|
||||
<Field
|
||||
label="Update instance interval (seconds)"
|
||||
description="How often shall the instance pull updates from GitHub?"
|
||||
label={t(
|
||||
'provisioning.finish-step.label-update-instance-interval-seconds',
|
||||
'Update instance interval (seconds)'
|
||||
)}
|
||||
description={t(
|
||||
'provisioning.finish-step.description-often-shall-instance-updates-git-hub',
|
||||
'How often shall the instance pull updates from GitHub?'
|
||||
)}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
{...register('repository.sync.intervalSeconds', { valueAsNumber: true })}
|
||||
type="number"
|
||||
placeholder="60"
|
||||
placeholder={t('provisioning.finish-step.placeholder', '60')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Workflows"
|
||||
description="Select the workflows that are allowed within this repository"
|
||||
label={t('provisioning.finish-step.label-workflows', 'Workflows')}
|
||||
description={t(
|
||||
'provisioning.finish-step.description-select-workflows-allowed-within-repository',
|
||||
'Select the workflows that are allowed within this repository'
|
||||
)}
|
||||
required
|
||||
error={errors.repository?.workflows?.message}
|
||||
invalid={!!errors.repository?.workflows}
|
||||
@@ -64,11 +74,11 @@ export function FinishStep() {
|
||||
<Controller
|
||||
name="repository.workflows"
|
||||
control={control}
|
||||
rules={{ required: 'This field is required.' }}
|
||||
rules={{ required: t('provisioning.finish-step.error-field-required', 'This field is required.') }}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<MultiCombobox
|
||||
options={getWorkflowOptions(type)}
|
||||
placeholder="Read-only repository"
|
||||
placeholder={t('provisioning.finish-step.placeholder-readonly-repository', 'Read-only repository')}
|
||||
onChange={(val) => {
|
||||
onChange(val.map((v) => v.value));
|
||||
}}
|
||||
@@ -80,8 +90,16 @@ export function FinishStep() {
|
||||
|
||||
{isGithub && false /* TODO */ && (
|
||||
<Field
|
||||
label={'Enable webhooks on changes' /* TODO: Link to docs when !isPublic */}
|
||||
description="Enable webhooks to automatically notify Grafana when a change occurs in the repository. This will allow Grafana to pull changes as soon as they are made."
|
||||
label={
|
||||
t(
|
||||
'provisioning.finish-step.label-enable-webhooks',
|
||||
'Enable webhooks on changes'
|
||||
) /* TODO: Link to docs when !isPublic */
|
||||
}
|
||||
description={t(
|
||||
'provisioning.finish-step.description-enable-webhooks',
|
||||
'Enable webhooks to automatically notify Grafana when a change occurs in the repository. This will allow Grafana to pull changes as soon as they are made.'
|
||||
)}
|
||||
disabled={!isPublic}
|
||||
>
|
||||
{/* TODO: Make an option for the switch to control */}
|
||||
@@ -94,17 +112,23 @@ export function FinishStep() {
|
||||
useLabel
|
||||
label={
|
||||
<span>
|
||||
Enable dashboard previews in pull requests{' '}
|
||||
{t(
|
||||
'provisioning.finish-step.label-enable-dashboard-previews',
|
||||
'Enable dashboard previews in pull requests'
|
||||
)}{' '}
|
||||
<span className={style.explanation}>
|
||||
(Requires image rendering.{' '}
|
||||
{t('provisioning.finish-step.text-requires-image-rendering', '(Requires image rendering.')}{' '}
|
||||
<a className={style.explanationLink} href="https://grafana.com">
|
||||
Set up image rendering
|
||||
{t('provisioning.finish-step.link-setup-image-rendering', 'Set up image rendering')}
|
||||
</a>
|
||||
)
|
||||
{')'}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
description="Adds an image preview of dashboard changes in pull requests. Images of your Grafana dashboards will be shared in your Git repository and visible to anyone with repository access."
|
||||
description={t(
|
||||
'provisioning.finish-step.description-dashboard-previews',
|
||||
'Adds an image preview of dashboard changes in pull requests. Images of your Grafana dashboards will be shared in your Git repository and visible to anyone with repository access.'
|
||||
)}
|
||||
disabled={!hasImageRenderer || !isPublic}
|
||||
>
|
||||
<Switch {...register('repository.generateDashboardPreviews')} id="repository.generateDashboardPreviews" />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useFormContext } from 'react-hook-form';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { Stack, Text } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { JobStatus } from '../Job/JobStatus';
|
||||
import { StepStatus, useStepStatus } from '../hooks/useStepStatus';
|
||||
@@ -37,11 +38,14 @@ export function JobStep({ onStepUpdate, description, startJob, children }: JobSt
|
||||
try {
|
||||
const response = await startJob(repositoryName);
|
||||
if (!response?.metadata?.name) {
|
||||
throw new Error('Invalid response from operation');
|
||||
throw new Error(t('provisioning.job-step.error-invalid-response', 'Invalid response from operation'));
|
||||
}
|
||||
setJobName(response.metadata.name);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to start operation';
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('provisioning.job-step.error-failed-to-start', 'Failed to start operation');
|
||||
stepStatus.setError(errorMessage);
|
||||
throw error; // Re-throw to mark the async operation as failed
|
||||
}
|
||||
@@ -59,7 +63,7 @@ export function JobStep({ onStepUpdate, description, startJob, children }: JobSt
|
||||
if (success) {
|
||||
stepStatus.setSuccess();
|
||||
} else {
|
||||
stepStatus.setError('Job failed');
|
||||
stepStatus.setError(t('provisioning.job-step.error-job-failed', 'Job failed'));
|
||||
}
|
||||
}}
|
||||
onRunningChange={(isRunning) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useCreateRepositoryMigrateMutation } from 'app/api/clients/provisioning';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { StepStatus } from '../hooks/useStepStatus';
|
||||
|
||||
@@ -29,8 +30,10 @@ export function MigrateStep({ onStepUpdate }: MigrateStepProps) {
|
||||
return (
|
||||
<JobStep
|
||||
onStepUpdate={onStepUpdate}
|
||||
description="Migrating all dashboards from this instance to your repository, including their identifiers and complete
|
||||
history. After this one-time migration, all future updates will be automatically saved to the repository."
|
||||
description={t(
|
||||
'provisioning.migrate-step.description-migrating-dashboards',
|
||||
'Migrating all dashboards from this instance to your repository, including their identifiers and complete history. After this one-time migration, all future updates will be automatically saved to the repository.'
|
||||
)}
|
||||
startJob={startMigration}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { getDefaultValues } from '../Config/ConfigForm';
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
@@ -11,14 +12,6 @@ import { Step } from './Stepper';
|
||||
import { WizardContent } from './WizardContent';
|
||||
import { WizardFormData, WizardStep } from './types';
|
||||
|
||||
const steps: Array<Step<WizardStep>> = [
|
||||
{ id: 'connection', name: 'Connect', title: 'Connect to external storage', submitOnNext: true },
|
||||
{ id: 'bootstrap', name: 'Bootstrap', title: 'Bootstrap repository', submitOnNext: true },
|
||||
{ id: 'migrate', name: 'Resources', title: 'Migrate resources', submitOnNext: false },
|
||||
{ id: 'pull', name: 'Resources', title: 'Pull resources', submitOnNext: false },
|
||||
{ id: 'finish', name: 'Finish', title: 'Finish setup', submitOnNext: true },
|
||||
];
|
||||
|
||||
export function ProvisioningWizard() {
|
||||
const [activeStep, setActiveStep] = useState<WizardStep>('connection');
|
||||
const [completedSteps, setCompletedSteps] = useState<WizardStep[]>([]);
|
||||
@@ -28,6 +21,42 @@ export function ProvisioningWizard() {
|
||||
const navigate = useNavigate();
|
||||
const values = getDefaultValues();
|
||||
|
||||
const steps = useMemo<Array<Step<WizardStep>>>(
|
||||
() => [
|
||||
{
|
||||
id: 'connection',
|
||||
name: t('provisioning.wizard.step-connect', 'Connect'),
|
||||
title: t('provisioning.wizard.title-connect', 'Connect to external storage'),
|
||||
submitOnNext: true,
|
||||
},
|
||||
{
|
||||
id: 'bootstrap',
|
||||
name: t('provisioning.wizard.step-bootstrap', 'Bootstrap'),
|
||||
title: t('provisioning.wizard.title-bootstrap', 'Bootstrap repository'),
|
||||
submitOnNext: true,
|
||||
},
|
||||
{
|
||||
id: 'migrate',
|
||||
name: t('provisioning.wizard.step-resources', 'Resources'),
|
||||
title: t('provisioning.wizard.title-migrate', 'Migrate resources'),
|
||||
submitOnNext: false,
|
||||
},
|
||||
{
|
||||
id: 'pull',
|
||||
name: t('provisioning.wizard.step-resources', 'Resources'),
|
||||
title: t('provisioning.wizard.title-pull', 'Pull resources'),
|
||||
submitOnNext: false,
|
||||
},
|
||||
{
|
||||
id: 'finish',
|
||||
name: t('provisioning.wizard.step-finish', 'Finish'),
|
||||
title: t('provisioning.wizard.title-finish', 'Finish setup'),
|
||||
submitOnNext: true,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const methods = useForm<WizardFormData>({
|
||||
defaultValues: {
|
||||
repository: values,
|
||||
@@ -53,16 +82,21 @@ export function ProvisioningWizard() {
|
||||
return requiresMigration
|
||||
? steps.filter((step) => step.id !== 'pull')
|
||||
: steps.filter((step) => step.id !== 'migrate');
|
||||
}, [requiresMigration]);
|
||||
}, [requiresMigration, steps]);
|
||||
|
||||
// Calculate button text based on current step position
|
||||
const getNextButtonText = (currentStep: WizardStep) => {
|
||||
const stepIndex = availableSteps.findIndex((s) => s.id === currentStep);
|
||||
if (currentStep === 'bootstrap') {
|
||||
return 'Start';
|
||||
}
|
||||
return stepIndex === availableSteps.length - 1 ? 'Finish' : 'Next';
|
||||
};
|
||||
const getNextButtonText = useCallback(
|
||||
(currentStep: WizardStep) => {
|
||||
const stepIndex = availableSteps.findIndex((s) => s.id === currentStep);
|
||||
if (currentStep === 'bootstrap') {
|
||||
return t('provisioning.wizard.button-start', 'Start');
|
||||
}
|
||||
return stepIndex === availableSteps.length - 1
|
||||
? t('provisioning.wizard.button-finish', 'Finish')
|
||||
: t('provisioning.wizard.button-next', 'Next');
|
||||
},
|
||||
[availableSteps]
|
||||
);
|
||||
|
||||
const handleNext = async () => {
|
||||
const currentStepIndex = availableSteps.findIndex((s) => s.id === activeStep);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCreateRepositorySyncMutation } from 'app/api/clients/provisioning';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { StepStatus } from '../hooks/useStepStatus';
|
||||
|
||||
@@ -22,7 +23,10 @@ export function PullStep({ onStepUpdate }: PullStepProps) {
|
||||
return (
|
||||
<JobStep
|
||||
onStepUpdate={onStepUpdate}
|
||||
description="Pulling all content from your repository to this Grafana instance. This ensures your dashboards and other resources are synchronized with the repository."
|
||||
description={t(
|
||||
'provisioning.pull-step.description-pulling-content',
|
||||
'Pulling all content from your repository to this Grafana instance. This ensures your dashboards and other resources are synchronized with the repository.'
|
||||
)}
|
||||
startJob={startSync}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
|
||||
interface RequestErrorAlertProps {
|
||||
@@ -13,13 +14,13 @@ interface RequestErrorAlertProps {
|
||||
function getDefaultTitle(endpointName?: string): string {
|
||||
switch (endpointName) {
|
||||
case 'createRepositorySync':
|
||||
return 'Failed to sync dashboards';
|
||||
return t('provisioning.request-error.failed-to-sync', 'Failed to sync dashboards');
|
||||
case 'createRepositoryMigrate':
|
||||
return 'Failed to migrate dashboards';
|
||||
return t('provisioning.request-error.failed-to-migrate', 'Failed to migrate dashboards');
|
||||
case 'createOrUpdateRepository':
|
||||
return 'Failed to save repository';
|
||||
return t('provisioning.request-error.failed-to-save', 'Failed to save repository');
|
||||
default:
|
||||
return 'Operation failed';
|
||||
return t('provisioning.request-error.operation-failed', 'Operation failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export function Stepper<T extends string | number>({
|
||||
{successField && <Icon name={'check'} size={'xl'} className={styles.successItem} />}
|
||||
{warnField && <Icon name={'exclamation-triangle'} className={styles.warnItem} />}
|
||||
<div className={styles.link}>{step.name}</div>
|
||||
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
|
||||
{!isLast && <div className={styles.divider}>—</div>}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useDeleteRepositoryMutation,
|
||||
useGetFrontendSettingsQuery,
|
||||
} from 'app/api/clients/provisioning';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
import { useCreateOrUpdateRepository } from '../hooks';
|
||||
@@ -79,7 +80,9 @@ export function WizardContent({
|
||||
if (settingsQuery.data?.items.some((item) => item.target === 'instance' && item.name !== repoName)) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: ['Instance repository already exists'],
|
||||
payload: [
|
||||
t('provisioning.wizard-content.error-instance-repository-exists', 'Instance repository already exists'),
|
||||
],
|
||||
});
|
||||
if (repoName) {
|
||||
console.warn('Should we delete the pending repo?', repoName);
|
||||
@@ -93,14 +96,16 @@ export function WizardContent({
|
||||
await deleteRepository({ name });
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Repository deleted'],
|
||||
payload: [t('provisioning.wizard-content.success-repository-deleted', 'Repository deleted')],
|
||||
});
|
||||
// Wait before redirecting to ensure deletion is indexed
|
||||
setTimeout(() => navigate(PROVISIONING_URL), 1500);
|
||||
} catch (error) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: ['Failed to delete repository. Please try again.'],
|
||||
payload: [
|
||||
t('provisioning.wizard-content.error-failed-to-delete', 'Failed to delete repository. Please try again.'),
|
||||
],
|
||||
});
|
||||
setIsCancelling(false);
|
||||
}
|
||||
@@ -165,7 +170,7 @@ export function WizardContent({
|
||||
setValue('repositoryName', newName);
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Repository saved'],
|
||||
payload: [t('provisioning.wizard-content.success-repository-saved', 'Repository saved')],
|
||||
});
|
||||
handleStatusChange(true);
|
||||
}
|
||||
@@ -193,12 +198,16 @@ export function WizardContent({
|
||||
}}
|
||||
/>
|
||||
<Box marginBottom={2}>
|
||||
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
|
||||
<Text element="h2">
|
||||
{currentStepIndex + 1}. {currentStep?.title}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<RequestErrorAlert request={saveRequest} title="Repository verification failed" />
|
||||
<RequestErrorAlert
|
||||
request={saveRequest}
|
||||
title={t('provisioning.wizard-content.title-repository-verification-failed', 'Repository verification failed')}
|
||||
/>
|
||||
|
||||
<div className={styles.content}>
|
||||
{activeStep === 'connection' && <ConnectStep />}
|
||||
@@ -223,10 +232,14 @@ export function WizardContent({
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting || isCancelling}
|
||||
>
|
||||
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
||||
{isCancelling
|
||||
? t('provisioning.wizard-content.button-cancelling', 'Cancelling...')
|
||||
: t('provisioning.wizard-content.button-cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleNextWithSubmit} disabled={isNextButtonDisabled()}>
|
||||
{isSubmitting ? 'Submitting...' : getNextButtonText(activeStep)}
|
||||
{isSubmitting
|
||||
? t('provisioning.wizard-content.button-submitting', 'Submitting...')
|
||||
: getNextButtonText(activeStep)}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@@ -1106,13 +1106,34 @@
|
||||
"create-label": "Create",
|
||||
"name-label": "Folder name"
|
||||
},
|
||||
"new-provisioned-folder-form": {
|
||||
"alert-error-creating-folder": "Error creating folder",
|
||||
"alert-folder-created-successfully": "Folder created successfully",
|
||||
"button-create": "Create",
|
||||
"button-creating": "Creating...",
|
||||
"cancel": "Cancel",
|
||||
"description-branch-name-in-git-hub": "Branch name in GitHub",
|
||||
"error-invalid-folder-name": "Invalid folder name",
|
||||
"error-required": "Folder name is required",
|
||||
"folder-comment-input-placeholder-describe-changes-optional": "Add a note to describe your changes (optional)",
|
||||
"folder-name-input-placeholder-enter-folder-name": "Enter folder name",
|
||||
"label-branch": "Branch",
|
||||
"label-comment": "Comment",
|
||||
"label-folder-name": "Folder name",
|
||||
"label-workflow": "Workflow",
|
||||
"text-pull-request-created": "A pull request has been created with changes to this folder:",
|
||||
"title-pull-request-created": "Pull request created",
|
||||
"title-repository-not-found": "Repository not found",
|
||||
"title-this-repository-is-read-only": "This repository is read only"
|
||||
},
|
||||
"no-results": {
|
||||
"clear": "Clear search and filters",
|
||||
"text": "No results found for your query"
|
||||
},
|
||||
"soft-delete": {
|
||||
"success": "Dashboard {{name}} moved to Recently deleted"
|
||||
}
|
||||
},
|
||||
"text-this-repository-is-read-only": "If you have direct access to the target, copy the JSON and paste it there."
|
||||
},
|
||||
"carousel": {
|
||||
"close": "Close",
|
||||
@@ -2600,6 +2621,9 @@
|
||||
"create-instructions": "Press enter to create the new folder.",
|
||||
"loading": "Loading folders..."
|
||||
},
|
||||
"folder-repo": {
|
||||
"badge-text": "Provisioned"
|
||||
},
|
||||
"forgot-password": {
|
||||
"back-button": "Back to login",
|
||||
"change-password": {
|
||||
@@ -4424,6 +4448,391 @@
|
||||
"text-loading-teams": "Loading teams..."
|
||||
}
|
||||
},
|
||||
"provisioning": {
|
||||
"bootstrap-step": {
|
||||
"dashboards-unavailable-while-running-process": "Dashboards will be unavailable while running this process",
|
||||
"description-clear-repository-connection": "Add a clear name for this repository connection",
|
||||
"empty": "Empty",
|
||||
"error-field-required": "This field is required.",
|
||||
"files-count_one": "{{count}} files",
|
||||
"files-count_other": "{{count}} files",
|
||||
"grafana": "Grafana",
|
||||
"include-history": "Include history",
|
||||
"include-identifiers": "Include identifiers",
|
||||
"label-display-name": "Display name",
|
||||
"label-migrate-options": "Migrate options",
|
||||
"placeholder-my-repository-connection": "My repository connection",
|
||||
"repository": "Repository",
|
||||
"resources-will-be-added": "The {{count}} resources in grafana will be added to the repository. Grafana will then include both the current resources and anything from the repository when done.",
|
||||
"text-loading-resource-information": "Loading resource information...",
|
||||
"title-files-exist-in-the-target": "Files exist in the target",
|
||||
"title-note": "Note",
|
||||
"tooltip-include-history": "Include complete dashboard version history",
|
||||
"tooltip-include-identifiers": "Include unique identifiers for each dashboard to maintain references"
|
||||
},
|
||||
"check-repository": {
|
||||
"check": "Check"
|
||||
},
|
||||
"config-form": {
|
||||
"alert-repository-settings-saved": "Repository settings saved",
|
||||
"button-save": "Save",
|
||||
"button-saving": "Saving...",
|
||||
"description-branch": "Create a branch (and pull request) for changes",
|
||||
"description-enabled": "Once automatic pulling is enabled, the target cannot be changed.",
|
||||
"description-path": "Path to a subdirectory in the Git repository",
|
||||
"description-repository-url": "Enter the GitHub repository URL",
|
||||
"description-title": "A human-readable name for the config",
|
||||
"description-workflows-makes-repository": "No workflows makes the repository read only",
|
||||
"description-write": "Allow writing updates to the remote repository",
|
||||
"error-required": "This field is required.",
|
||||
"error-valid-github-url": "Please enter a valid GitHub repository URL",
|
||||
"label-automatic-pulling": "Automatic pulling",
|
||||
"label-branch": "Branch",
|
||||
"label-enabled": "Enabled",
|
||||
"label-github-token": "GitHub token",
|
||||
"label-interval-seconds": "Interval (seconds)",
|
||||
"label-local-path": "Local path",
|
||||
"label-path": "Path",
|
||||
"label-repository-type": "Repository type",
|
||||
"label-repository-url": "Repository URL",
|
||||
"label-target": "Target",
|
||||
"label-title": "Title",
|
||||
"label-workflows": "Workflows",
|
||||
"option-branch": "Branch",
|
||||
"option-entire-instance": "Entire instance",
|
||||
"option-github": "GitHub",
|
||||
"option-local": "Local",
|
||||
"option-managed-folder": "Managed folder",
|
||||
"option-write": "Write",
|
||||
"placeholder-branch": "main",
|
||||
"placeholder-github-token": "ghp_yourTokenHere1234567890abcdEFGHijklMNOP",
|
||||
"placeholder-github-url": "https://github.com/username/repo-name",
|
||||
"placeholder-interval-seconds": "60",
|
||||
"placeholder-local-path": "/path/to/repo",
|
||||
"placeholder-my-config": "My config",
|
||||
"placeholder-path": "grafana/",
|
||||
"placeholder-readonly-repository": "Readonly repository",
|
||||
"placeholder-select-repository-type": "Select repository type"
|
||||
},
|
||||
"config-form-github-collapse": {
|
||||
"description-attach-dashboard-previews": "Render before/after images and link them to the pull request.<1></1>NOTE: This will render dashboards into an image that can be access by a public URL",
|
||||
"instructions": "Instructions",
|
||||
"label-attach-dashboard-previews": "Attach dashboard previews to pull requests",
|
||||
"label-git-hub-features": "GitHub features",
|
||||
"pull-request-image-previews": "Pull Request image previews",
|
||||
"realtime-feedback": "Realtime feedback",
|
||||
"text-changes-in-git-eventually-pulled": "Changes in git will eventually be pulled depending on the synchronization interval. Pull requests will not be processed",
|
||||
"text-changes-in-git-quick-pull": "Changes in git will be quickly pulled into grafana. Pull requests can be processed.",
|
||||
"text-when-image-renderer-configured": "When the image renderer is configured, pull requests can see preview images",
|
||||
"title-image-renderer-not-configured": "Image renderer not configured",
|
||||
"title-public-url-not-configured": "Public URL not configured",
|
||||
"title-webhook-will-be-created": "Webhook will be created"
|
||||
},
|
||||
"connect-repository-button": {
|
||||
"connect-to-repository": "Connect to repository",
|
||||
"max-repositories-exist": "Maximum repositories exist ({{count}})",
|
||||
"tooltip-max-repos_one": "Max repositories already created ({{count}})",
|
||||
"tooltip-max-repos_other": "Max repositories already created ({{count}})"
|
||||
},
|
||||
"connect-step": {
|
||||
"description-choose-storage-resources": "Choose the type of storage for your resources",
|
||||
"description-github-path": "Path to a subdirectory in the Git repository",
|
||||
"description-paste-your-git-hub-personal-access-token": "Paste your GitHub personal access token",
|
||||
"description-repository-url": "Paste the URL of your GitHub repository",
|
||||
"error-field-required": "This field is required.",
|
||||
"error-invalid-github-url": "Please enter a valid GitHub repository URL",
|
||||
"label-access-token": "Enter your access token",
|
||||
"label-branch": "Branch",
|
||||
"label-local-path": "Local path",
|
||||
"label-path": "Path",
|
||||
"label-repository-url": "Enter your Repository URL",
|
||||
"label-storage-type": "Storage type",
|
||||
"placeholder-branch": "main",
|
||||
"placeholder-github-path": "grafana/",
|
||||
"placeholder-github-token": "github_pat_yourTokenHere1234567890abcdEFGHijklMNOP",
|
||||
"placeholder-github-url": "https://github.com/username/repo",
|
||||
"placeholder-local-path": "/path/to/repo",
|
||||
"storage-type-github": "GitHub",
|
||||
"storage-type-local": "Local"
|
||||
},
|
||||
"delete-repository-button": {
|
||||
"button-delete": "Delete",
|
||||
"confirm-delete-repository": "Are you sure you want to delete the repository config?",
|
||||
"success-repository-deleted": "Repository settings queued for deletion",
|
||||
"title-delete-repository": "Delete repository config",
|
||||
"tooltip-delete-this-repository": "Delete this repository"
|
||||
},
|
||||
"edit-repository-page": {
|
||||
"back-to-repositories": "Back to repositories",
|
||||
"repository-config-exists-configuration": "Make sure the repository config exists in the configuration file."
|
||||
},
|
||||
"empty-state": {
|
||||
"no-jobs": "No jobs..."
|
||||
},
|
||||
"enhanced-features": {
|
||||
"description-instant-updates": "Get instant updates in Grafana as soon as changes are committed. Review and approve changes using pull requests before they go live.",
|
||||
"description-visual-previews-dashboard-updates-directly-requests": "See visual previews of dashboard updates directly in pull requests",
|
||||
"set-up-image-rendering": "Set up image rendering",
|
||||
"set-up-public-access": "Set up public access",
|
||||
"title-instant-updates-requests-webhooks": "Instant updates and pull requests with webhooks",
|
||||
"title-visual-previews-in-pull-requests": "Visual previews in pull requests",
|
||||
"unlock-enhanced-functionality-for-git-hub": "Unlock enhanced functionality for GitHub"
|
||||
},
|
||||
"expanded-row": {
|
||||
"job-specification": "Job Specification",
|
||||
"summary": "Summary",
|
||||
"title-error": "Error"
|
||||
},
|
||||
"features-list": {
|
||||
"actions": {
|
||||
"set-up-required-feature-toggles": "Set up required feature toggles"
|
||||
},
|
||||
"automatically-provision-and-update-dashboards": "Automatically provision and update your dashboards as soon as changes are pushed to your GitHub repository",
|
||||
"learn-more": "Learn more",
|
||||
"manage-dashboards-provision-updates-automatically": "Manage dashboards as code and provision updates automatically",
|
||||
"manage-your-dashboards-with-remote-provisioning": "Manage your dashboards with remote provisioning",
|
||||
"migrate-existing-dashboards-storage-provisioning": "Migrate existing dashboards to storage for provisioning",
|
||||
"store-dashboards-in-version-controlled-storage": "Store dashboards in version-controlled storage for better organization and history tracking",
|
||||
"visual-previews-in-pull-requests": "Visual previews in pull requests to review your changes before going live"
|
||||
},
|
||||
"file-history-page": {
|
||||
"back-to-repositories": "Back to repositories",
|
||||
"repository-config-exists-configuration": "Make sure the repository config exists in the configuration file."
|
||||
},
|
||||
"file-status-page": {
|
||||
"title-error-loading-file": "Error loading file"
|
||||
},
|
||||
"files-view": {
|
||||
"columns": {
|
||||
"history": "History",
|
||||
"view": "View"
|
||||
},
|
||||
"placeholder-search": "Search"
|
||||
},
|
||||
"finish-step": {
|
||||
"description-dashboard-previews": "Adds an image preview of dashboard changes in pull requests. Images of your Grafana dashboards will be shared in your Git repository and visible to anyone with repository access.",
|
||||
"description-enable-webhooks": "Enable webhooks to automatically notify Grafana when a change occurs in the repository. This will allow Grafana to pull changes as soon as they are made.",
|
||||
"description-often-shall-instance-updates-git-hub": "How often shall the instance pull updates from GitHub?",
|
||||
"description-select-workflows-allowed-within-repository": "Select the workflows that are allowed within this repository",
|
||||
"error-field-required": "This field is required.",
|
||||
"label-enable-dashboard-previews": "Enable dashboard previews in pull requests",
|
||||
"label-enable-webhooks": "Enable webhooks on changes",
|
||||
"label-update-instance-interval-seconds": "Update instance interval (seconds)",
|
||||
"label-workflows": "Workflows",
|
||||
"link-setup-image-rendering": "Set up image rendering",
|
||||
"placeholder": "60",
|
||||
"placeholder-readonly-repository": "Read-only repository",
|
||||
"text-requires-image-rendering": "(Requires image rendering."
|
||||
},
|
||||
"folder-repository-list": {
|
||||
"no-results-matching-your-query": "No results matching your query",
|
||||
"placeholder-search": "Search"
|
||||
},
|
||||
"getting-started": {
|
||||
"alert-temporary-outage": "When you connect your whole instance, dashboards will be unavailable while running the migration. We recommend warning your users before starting the process.",
|
||||
"engaging-graphic": "Engaging graphic",
|
||||
"modal-description-public-access": "Set up public access to your Grafana instance to enable GitHub integration",
|
||||
"modal-description-required-features": "Enable required Grafana features for provisioning",
|
||||
"modal-title-set-up-public-access": "Set up public access",
|
||||
"modal-title-set-up-required-features": "Set up required features",
|
||||
"step-description-copy-url": "From the ngrok output, copy the https:// forwarding URL that looks like this:",
|
||||
"step-description-enable-feature-toggles": "Add these settings to your custom.ini file to enable necessary features:",
|
||||
"step-description-start-ngrok": "Run this command to create a secure tunnel to your local Grafana:",
|
||||
"step-description-update-grafana-config": "Add this to your custom.ini file, replacing the URL with your actual ngrok URL:",
|
||||
"step-title-copy-url": "Copy your public URL",
|
||||
"step-title-enable-feature-toggles": "Enable Required Feature Toggles",
|
||||
"step-title-start-ngrok": "Start ngrok for temporary public access",
|
||||
"step-title-update-grafana-config": "Update your Grafana configuration",
|
||||
"title-setting-connection-could-cause-temporary-outage": "Setting up this connection could cause a temporary outage"
|
||||
},
|
||||
"getting-started-page": {
|
||||
"subtitle-provisioning-feature": "Provisioning is a feature that allows you to manage your dashboards using GitHub and other storage systems",
|
||||
"text-remote-provisioning": "Remote provisioning"
|
||||
},
|
||||
"history-view": {
|
||||
"not-found": "Not found"
|
||||
},
|
||||
"home-page": {
|
||||
"button-delete-repositories": "Delete repositories",
|
||||
"configured-repositories-while-running-legacy-storage": "Configured repositories will not work while running legacy storage.",
|
||||
"confirm-delete-repositories": "Are you sure you want to delete all configured repositories? This action cannot be undone.",
|
||||
"remove-all-configured-repositories": "Remove all configured repositories",
|
||||
"subtitle": "View and manage your configured repositories",
|
||||
"success-all-repositories-deleted": "All configured repositories deleted",
|
||||
"tab-files": "Files",
|
||||
"tab-files-title": "The raw file list from the repository",
|
||||
"tab-getting-started": "Getting started",
|
||||
"tab-getting-started-title": "Getting started",
|
||||
"tab-overview": "Overview",
|
||||
"tab-overview-title": "Repository overview",
|
||||
"tab-repositories": "Repositories",
|
||||
"tab-repositories-title": "List of repositories",
|
||||
"tab-resources": "Resources",
|
||||
"tab-resources-title": "Resources saved in Grafana database",
|
||||
"title-delete-all-configured-repositories": "Delete all configured repositories",
|
||||
"title-legacy-storage-detected": "Legacy storage detected"
|
||||
},
|
||||
"job-status": {
|
||||
"error-unknown": "An unknown error occurred",
|
||||
"label-view-details": "View details",
|
||||
"starting": "Starting...",
|
||||
"status": {
|
||||
"title-error-running-job": "error running job",
|
||||
"title-job-completed-successfully": "Job completed successfully"
|
||||
},
|
||||
"summary": "Summary"
|
||||
},
|
||||
"job-step": {
|
||||
"error-failed-to-start": "Failed to start operation",
|
||||
"error-invalid-response": "Invalid response from operation",
|
||||
"error-job-failed": "Job failed"
|
||||
},
|
||||
"migrate-step": {
|
||||
"description-migrating-dashboards": "Migrating all dashboards from this instance to your repository, including their identifiers and complete history. After this one-time migration, all future updates will be automatically saved to the repository."
|
||||
},
|
||||
"pull-step": {
|
||||
"description-pulling-content": "Pulling all content from your repository to this Grafana instance. This ensures your dashboards and other resources are synchronized with the repository."
|
||||
},
|
||||
"recent-jobs": {
|
||||
"active-jobs": "active jobs",
|
||||
"column-action": "Action",
|
||||
"column-duration": "Duration",
|
||||
"column-message": "Message",
|
||||
"column-started": "Started",
|
||||
"column-status": "Status",
|
||||
"error-loading": "Error loading {{type}}",
|
||||
"jobs": "Jobs"
|
||||
},
|
||||
"repository-actions": {
|
||||
"settings": "Settings",
|
||||
"source-code": "Source Code"
|
||||
},
|
||||
"repository-card": {
|
||||
"get-repository-meta": {
|
||||
"webhook": "Webhook"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"view": "View"
|
||||
},
|
||||
"repository-health": {
|
||||
"details": "Details:",
|
||||
"no-errors-found": "No errors found",
|
||||
"title-repository-is-healthy": "Repository is healthy",
|
||||
"title-repository-is-unhealthy": "Repository is unhealthy"
|
||||
},
|
||||
"repository-link": {
|
||||
"grafana-repository": "Grafana and your repository are now in sync.",
|
||||
"view-folder": "View folder",
|
||||
"view-repository": "View repository"
|
||||
},
|
||||
"repository-overview": {
|
||||
"checked": "Checked:",
|
||||
"finished": "Finished:",
|
||||
"health": "Health",
|
||||
"job-id": "Job ID:",
|
||||
"last-ref": "Last Ref:",
|
||||
"messages": "Messages:",
|
||||
"pull-status": "Pull status",
|
||||
"resources": "Resources",
|
||||
"started": "Started:",
|
||||
"status": "Status:",
|
||||
"view-folder": "View Folder",
|
||||
"webhook": "Webhook"
|
||||
},
|
||||
"repository-resources": {
|
||||
"columns": {
|
||||
"history": "History",
|
||||
"view-dashboard": "View",
|
||||
"view-folder": "View"
|
||||
},
|
||||
"placeholder-search": "Search"
|
||||
},
|
||||
"repository-status-page": {
|
||||
"back-to-repositories": "Back to repositories",
|
||||
"cleaning-up-resources": "Cleaning up repository resources",
|
||||
"legacy-storage-message": "Instance is not yet running unified storage -- requires migration wizard",
|
||||
"not-found": "not found",
|
||||
"not-found-message": "Repository not found",
|
||||
"repository-config-exists-configuration": "Make sure the repository config exists in the configuration file.",
|
||||
"tab-files": "Files",
|
||||
"tab-files-title": "The raw file list from the repository",
|
||||
"tab-overview": "Overview",
|
||||
"tab-overview-title": "Repository overview",
|
||||
"tab-resources": "Resources",
|
||||
"tab-resources-title": "Resources saved in grafana database",
|
||||
"title": "Repository Status",
|
||||
"title-legacy-storage": "Legacy Storage",
|
||||
"title-queued-for-deletion": "Queued for deletion"
|
||||
},
|
||||
"request-error": {
|
||||
"failed-to-migrate": "Failed to migrate dashboards",
|
||||
"failed-to-save": "Failed to save repository",
|
||||
"failed-to-sync": "Failed to sync dashboards",
|
||||
"operation-failed": "Operation failed"
|
||||
},
|
||||
"resource-view": {
|
||||
"base": "Base",
|
||||
"dashboard-preview": "Dashboard Preview",
|
||||
"existing-dashboard": "Existing dashboard",
|
||||
"history": "History",
|
||||
"repository": "Repository",
|
||||
"title-error-saving-file": "Error saving file"
|
||||
},
|
||||
"setup-modal": {
|
||||
"done": "Done",
|
||||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"sidebar-item": {
|
||||
"label-completed-step": "Completed step",
|
||||
"label-current-step": "Current step",
|
||||
"label-pending-step": "Pending step"
|
||||
},
|
||||
"sync-repository": {
|
||||
"body-edit-configuration": "Edit the configuration",
|
||||
"button-edit": "Edit",
|
||||
"error-pulling-resources": "Error pulling resources",
|
||||
"pull": "Pull",
|
||||
"success-pull-started": "Pull started",
|
||||
"title-pull-not-enabled": "Pull is not enabled",
|
||||
"tooltip-unhealthy-repository": "Unable to pull an unhealthy repository"
|
||||
},
|
||||
"token-permissions-info": {
|
||||
"access": "Access",
|
||||
"contents": "Contents",
|
||||
"github-instructions": "Go to <2>GitHub Personal Access Tokens</2>. Make sure to include these permissions under <4>Repository</4>:",
|
||||
"metadata": "Metadata",
|
||||
"permission": "Permission",
|
||||
"pull-requests": "Pull requests",
|
||||
"read-and-write": "Read and write",
|
||||
"readonly": "Read-only",
|
||||
"webhooks": "Webhooks"
|
||||
},
|
||||
"wizard": {
|
||||
"button-finish": "Finish",
|
||||
"button-next": "Next",
|
||||
"button-start": "Start",
|
||||
"step-bootstrap": "Bootstrap",
|
||||
"step-connect": "Connect",
|
||||
"step-finish": "Finish",
|
||||
"step-resources": "Resources",
|
||||
"title-bootstrap": "Bootstrap repository",
|
||||
"title-connect": "Connect to external storage",
|
||||
"title-finish": "Finish setup",
|
||||
"title-migrate": "Migrate resources",
|
||||
"title-pull": "Pull resources"
|
||||
},
|
||||
"wizard-content": {
|
||||
"button-cancel": "Cancel",
|
||||
"button-cancelling": "Cancelling...",
|
||||
"button-submitting": "Submitting...",
|
||||
"error-failed-to-delete": "Failed to delete repository. Please try again.",
|
||||
"error-instance-repository-exists": "Instance repository already exists",
|
||||
"success-repository-deleted": "Repository deleted",
|
||||
"success-repository-saved": "Repository saved",
|
||||
"title-repository-verification-failed": "Repository verification failed"
|
||||
}
|
||||
},
|
||||
"public-dashboard": {
|
||||
"acknowledgment-checkboxes": {
|
||||
"ack-title": "Before you make the dashboard public, acknowledge the following:",
|
||||
|
||||
Reference in New Issue
Block a user