Provisioning: Add translations (#103111)

pull/104486/head
Alex Khomenko 4 months ago committed by GitHub
parent e9c78d6f5c
commit 40f6f3e6bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 281
      .betterer.results
  2. 3
      public/app/core/components/NestedFolderPicker/FolderRepo.tsx
  3. 97
      public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx
  4. 29
      public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx
  5. 155
      public/app/features/provisioning/Config/ConfigForm.tsx
  6. 65
      public/app/features/provisioning/Config/ConfigFormGithubCollapse.tsx
  7. 13
      public/app/features/provisioning/File/FileHistoryPage.tsx
  8. 19
      public/app/features/provisioning/File/FileStatusPage.tsx
  9. 16
      public/app/features/provisioning/File/FilesView.tsx
  10. 35
      public/app/features/provisioning/GettingStarted/EnhancedFeatures.tsx
  11. 43
      public/app/features/provisioning/GettingStarted/FeaturesList.tsx
  12. 70
      public/app/features/provisioning/GettingStarted/GettingStarted.tsx
  13. 9
      public/app/features/provisioning/GettingStarted/GettingStartedPage.tsx
  14. 7
      public/app/features/provisioning/GettingStarted/SetupModal.tsx
  15. 19
      public/app/features/provisioning/GettingStarted/SidebarItem.tsx
  16. 85
      public/app/features/provisioning/HomePage.tsx
  17. 35
      public/app/features/provisioning/Job/JobStatus.tsx
  18. 37
      public/app/features/provisioning/Job/RecentJobs.tsx
  19. 3
      public/app/features/provisioning/Repository/CheckRepository.tsx
  20. 19
      public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx
  21. 11
      public/app/features/provisioning/Repository/EditRepositoryPage.tsx
  22. 5
      public/app/features/provisioning/Repository/RepositoryActions.tsx
  23. 7
      public/app/features/provisioning/Repository/RepositoryCard.tsx
  24. 19
      public/app/features/provisioning/Repository/RepositoryHealth.tsx
  25. 53
      public/app/features/provisioning/Repository/RepositoryOverview.tsx
  26. 25
      public/app/features/provisioning/Repository/RepositoryResources.tsx
  27. 69
      public/app/features/provisioning/Repository/RepositoryStatusPage.tsx
  28. 22
      public/app/features/provisioning/Repository/SyncRepository.tsx
  29. 31
      public/app/features/provisioning/Shared/BranchValidationError.tsx
  30. 15
      public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx
  31. 13
      public/app/features/provisioning/Shared/FolderRepositoryList.tsx
  32. 53
      public/app/features/provisioning/Shared/TokenPermissionsInfo.tsx
  33. 78
      public/app/features/provisioning/Wizard/BootstrapStep.tsx
  34. 90
      public/app/features/provisioning/Wizard/ConnectStep.tsx
  35. 52
      public/app/features/provisioning/Wizard/FinishStep.tsx
  36. 10
      public/app/features/provisioning/Wizard/JobStep.tsx
  37. 7
      public/app/features/provisioning/Wizard/MigrateStep.tsx
  38. 66
      public/app/features/provisioning/Wizard/ProvisioningWizard.tsx
  39. 6
      public/app/features/provisioning/Wizard/PullStep.tsx
  40. 9
      public/app/features/provisioning/Wizard/RequestErrorAlert.tsx
  41. 1
      public/app/features/provisioning/Wizard/Stepper.tsx
  42. 27
      public/app/features/provisioning/Wizard/WizardContent.tsx
  43. 411
      public/locales/en-US/grafana.json

@ -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>
</>
);
};

@ -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>
<Trans i18nKey="provisioning.features-list.manage-dashboards-provision-updates-automatically">
Manage dashboards as code and provision updates automatically
</Trans>
</FeatureItem>
<FeatureItem>
Store dashboards in version-controlled storage for better organization and history tracking
<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}>&#8212;</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:",

Loading…
Cancel
Save