Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 688d6746c9 | |||
| e8f1eb1ee8 | |||
| c0f8e5688b | |||
| 6e7f28f5a1 | |||
| 9dcad9c255 | |||
| f8f4fb5640 | |||
| d5de92e5b2 |
@@ -1372,11 +1372,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/AlertLabelDropdown.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -1593,11 +1588,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
||||
@@ -37,6 +37,12 @@ export function SidebarResizer() {
|
||||
return;
|
||||
}
|
||||
|
||||
// mouse is moving with no buttons pressed
|
||||
if (!e.buttons) {
|
||||
dragStart.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = e.clientX - dragStart.current;
|
||||
dragStart.current = e.clientX;
|
||||
|
||||
|
||||
@@ -156,6 +156,9 @@ func (i *Identity) GetExtra() map[string][]string {
|
||||
if i.GetOrgRole().IsValid() {
|
||||
extra["user-instance-role"] = []string{string(i.GetOrgRole())}
|
||||
}
|
||||
if i.AccessTokenClaims != nil && i.AccessTokenClaims.Rest.ServiceIdentity != "" {
|
||||
extra[authn.ServiceIdentityKey] = []string{i.AccessTokenClaims.Rest.ServiceIdentity}
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
authnlib "github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIdentity_GetExtra(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
identity *Identity
|
||||
expected map[string][]string
|
||||
}{
|
||||
{
|
||||
name: "returns empty map when no extra fields are set",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeUser,
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"user-instance-role": {"None"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns id-token when IDToken is set",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeUser,
|
||||
IDToken: "test-id-token",
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"id-token": {"test-id-token"},
|
||||
"user-instance-role": {"None"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns user-instance-role when OrgRole is valid",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeUser,
|
||||
OrgID: 1,
|
||||
OrgRoles: map[int64]org.RoleType{1: "Admin"},
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"user-instance-role": {"Admin"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns service-identity when AccessTokenClaims contains ServiceIdentity",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeAccessPolicy,
|
||||
AccessTokenClaims: &authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
ServiceIdentity: "secrets-manager",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string][]string{
|
||||
string(authnlib.ServiceIdentityKey): {"secrets-manager"},
|
||||
"user-instance-role": {"None"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns all extra fields when multiple are set",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeUser,
|
||||
OrgID: 1,
|
||||
IDToken: "test-id-token",
|
||||
OrgRoles: map[int64]org.RoleType{1: "Editor"},
|
||||
AccessTokenClaims: &authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
ServiceIdentity: "custom-service",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"id-token": {"test-id-token"},
|
||||
"user-instance-role": {"Editor"},
|
||||
string(authnlib.ServiceIdentityKey): {"custom-service"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not include service-identity when AccessTokenClaims is nil",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeUser,
|
||||
AccessTokenClaims: nil,
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"user-instance-role": {"None"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not include service-identity when ServiceIdentity is empty",
|
||||
identity: &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeUser,
|
||||
AccessTokenClaims: &authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
ServiceIdentity: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"user-instance-role": {"None"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
extra := tt.identity.GetExtra()
|
||||
assert.Equal(t, tt.expected, extra)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentity_GetExtra_ServiceIdentityKey(t *testing.T) {
|
||||
// Test that the ServiceIdentityKey constant matches authlib's constant
|
||||
identity := &Identity{
|
||||
ID: "1",
|
||||
Type: types.TypeAccessPolicy,
|
||||
AccessTokenClaims: &authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
ServiceIdentity: "test-service",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
extra := identity.GetExtra()
|
||||
require.Contains(t, extra, string(authnlib.ServiceIdentityKey))
|
||||
assert.Equal(t, []string{"test-service"}, extra[string(authnlib.ServiceIdentityKey)])
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
[
|
||||
"CREATE TABLE `alert` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `panel_id` INT64 NOT NULL, `org_id` INT64 NOT NULL, `name` STRING(255) NOT NULL, `message` STRING(MAX) NOT NULL, `state` STRING(190) NOT NULL, `settings` STRING(MAX), `frequency` INT64 NOT NULL, `handler` INT64 NOT NULL, `severity` STRING(MAX) NOT NULL, `silenced` BOOL NOT NULL, `execution_error` STRING(MAX) NOT NULL, `eval_data` STRING(MAX), `eval_date` TIMESTAMP, `new_state_date` TIMESTAMP NOT NULL, `state_changes` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `for` INT64) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_alert_dashboard_id` ON `alert` (dashboard_id)",
|
||||
"CREATE INDEX `IDX_alert_org_id_id` ON `alert` (org_id, id)",
|
||||
"CREATE INDEX `IDX_alert_state` ON `alert` (state)",
|
||||
"CREATE TABLE `alert_configuration` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `alertmanager_configuration` STRING(MAX), `configuration_version` STRING(3) NOT NULL, `created_at` INT64 NOT NULL, `default` BOOL NOT NULL DEFAULT (false), `org_id` INT64 NOT NULL DEFAULT (0), `configuration_hash` STRING(32) NOT NULL DEFAULT ('not-yet-calculated')) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_configuration_org_id` ON `alert_configuration` (org_id)",
|
||||
"CREATE TABLE `alert_configuration_history` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL DEFAULT (0), `alertmanager_configuration` STRING(MAX) NOT NULL, `configuration_hash` STRING(32) NOT NULL DEFAULT ('not-yet-calculated'), `configuration_version` STRING(3) NOT NULL, `created_at` INT64 NOT NULL, `default` BOOL NOT NULL DEFAULT (false), `last_applied` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `alert_image` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `token` STRING(190) NOT NULL, `path` STRING(190) NOT NULL, `url` STRING(2048) NOT NULL, `created_at` TIMESTAMP NOT NULL, `expires_at` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_image_token` ON `alert_image` (token)",
|
||||
"CREATE TABLE `alert_instance` (`rule_org_id` INT64 NOT NULL, `rule_uid` STRING(40) NOT NULL, `labels` STRING(MAX) NOT NULL, `labels_hash` STRING(190) NOT NULL, `current_state` STRING(190) NOT NULL, `current_state_since` INT64 NOT NULL, `last_eval_time` INT64 NOT NULL, `current_state_end` INT64 NOT NULL DEFAULT (0), `current_reason` STRING(190), `result_fingerprint` STRING(16), `resolved_at` INT64, `last_sent_at` INT64) PRIMARY KEY (rule_org_id,rule_uid,labels_hash)",
|
||||
"CREATE INDEX `IDX_alert_instance_rule_org_id_current_state` ON `alert_instance` (rule_org_id, current_state)",
|
||||
"CREATE INDEX `IDX_alert_instance_rule_org_id_rule_uid_current_state` ON `alert_instance` (rule_org_id, rule_uid, current_state)",
|
||||
"CREATE TABLE `alert_notification` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `name` STRING(190) NOT NULL, `type` STRING(255) NOT NULL, `settings` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `is_default` BOOL NOT NULL DEFAULT (false), `frequency` INT64, `send_reminder` BOOL DEFAULT (false), `disable_resolve_message` BOOL NOT NULL DEFAULT (false), `uid` STRING(40), `secure_settings` STRING(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_notification_org_id_uid` ON `alert_notification` (org_id, uid)",
|
||||
"CREATE TABLE `alert_notification_state` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `alert_id` INT64 NOT NULL, `notifier_id` INT64 NOT NULL, `state` STRING(50) NOT NULL, `version` INT64 NOT NULL, `updated_at` INT64 NOT NULL, `alert_rule_state_updated_version` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_alert_notification_state_alert_id` ON `alert_notification_state` (alert_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_notification_state_org_id_alert_id_notifier_id` ON `alert_notification_state` (org_id, alert_id, notifier_id)",
|
||||
"CREATE TABLE `alert_rule` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `title` STRING(190) NOT NULL, `condition` STRING(190) NOT NULL, `data` STRING(MAX), `updated` TIMESTAMP NOT NULL, `interval_seconds` INT64 NOT NULL DEFAULT (60), `version` INT64 NOT NULL DEFAULT (0), `uid` STRING(40) NOT NULL DEFAULT ('0'), `namespace_uid` STRING(40) NOT NULL, `rule_group` STRING(190) NOT NULL, `no_data_state` STRING(15) NOT NULL DEFAULT ('NoData'), `exec_err_state` STRING(15) NOT NULL DEFAULT ('Alerting'), `for` INT64 NOT NULL DEFAULT (0), `annotations` STRING(MAX), `labels` STRING(MAX), `dashboard_uid` STRING(40), `panel_id` INT64, `rule_group_idx` INT64 NOT NULL DEFAULT (1), `is_paused` BOOL NOT NULL DEFAULT (false), `notification_settings` STRING(MAX), `record` STRING(MAX), `metadata` STRING(MAX), `updated_by` STRING(40), `guid` STRING(36) NOT NULL DEFAULT (''), `missing_series_evals_to_resolve` INT64) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_alert_rule_org_id_dashboard_uid_panel_id` ON `alert_rule` (org_id, dashboard_uid, panel_id)",
|
||||
"CREATE INDEX `IDX_alert_rule_org_id_namespace_uid_rule_group` ON `alert_rule` (org_id, namespace_uid, rule_group)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_guid` ON `alert_rule` (guid)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_org_id_namespace_uid_title` ON `alert_rule` (org_id, namespace_uid, title)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_org_id_uid` ON `alert_rule` (org_id, uid)",
|
||||
"CREATE TABLE `alert_rule_state` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `rule_uid` STRING(40) NOT NULL, `data` BYTES(MAX) NOT NULL, `updated_at` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_state_org_id_rule_uid` ON `alert_rule_state` (org_id, rule_uid)",
|
||||
"CREATE TABLE `alert_rule_tag` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `alert_id` INT64 NOT NULL, `tag_id` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_alert_rule_tag_alert_id` ON `alert_rule_tag` (alert_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_tag_alert_id_tag_id` ON `alert_rule_tag` (alert_id, tag_id)",
|
||||
"CREATE TABLE `alert_rule_version` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `rule_org_id` INT64 NOT NULL, `rule_uid` STRING(40) NOT NULL DEFAULT ('0'), `rule_namespace_uid` STRING(40) NOT NULL, `rule_group` STRING(190) NOT NULL, `parent_version` INT64 NOT NULL, `restored_from` INT64 NOT NULL, `version` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `title` STRING(190) NOT NULL, `condition` STRING(190) NOT NULL, `data` STRING(MAX), `interval_seconds` INT64 NOT NULL, `no_data_state` STRING(15) NOT NULL DEFAULT ('NoData'), `exec_err_state` STRING(15) NOT NULL DEFAULT ('Alerting'), `for` INT64 NOT NULL DEFAULT (0), `annotations` STRING(MAX), `labels` STRING(MAX), `rule_group_idx` INT64 NOT NULL DEFAULT (1), `is_paused` BOOL NOT NULL DEFAULT (false), `notification_settings` STRING(MAX), `record` STRING(MAX), `metadata` STRING(MAX), `created_by` STRING(40), `rule_guid` STRING(36) NOT NULL DEFAULT (''), `missing_series_evals_to_resolve` INT64) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_alert_rule_version_rule_org_id_rule_namespace_uid_rule_group` ON `alert_rule_version` (rule_org_id, rule_namespace_uid, rule_group)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_version_rule_guid_version` ON `alert_rule_version` (rule_guid, version)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_rule_version_rule_org_id_rule_uid_rule_guid_version` ON `alert_rule_version` (rule_org_id, rule_uid, rule_guid, version)",
|
||||
"CREATE TABLE `annotation` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `alert_id` INT64, `user_id` INT64, `dashboard_id` INT64, `panel_id` INT64, `category_id` INT64, `type` STRING(25) NOT NULL, `title` STRING(MAX) NOT NULL, `text` STRING(MAX) NOT NULL, `metric` STRING(255), `prev_state` STRING(40) NOT NULL, `new_state` STRING(40) NOT NULL, `data` STRING(MAX) NOT NULL, `epoch` INT64 NOT NULL, `region_id` INT64 DEFAULT (0), `tags` STRING(4096), `created` INT64 DEFAULT (0), `updated` INT64 DEFAULT (0), `epoch_end` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_annotation_alert_id` ON `annotation` (alert_id)",
|
||||
"CREATE INDEX `IDX_annotation_org_id_alert_id` ON `annotation` (org_id, alert_id)",
|
||||
"CREATE INDEX `IDX_annotation_org_id_created` ON `annotation` (org_id, created)",
|
||||
"CREATE INDEX `IDX_annotation_org_id_dashboard_id_epoch_end_epoch` ON `annotation` (org_id, dashboard_id, epoch_end, epoch)",
|
||||
"CREATE INDEX `IDX_annotation_org_id_epoch_end_epoch` ON `annotation` (org_id, epoch_end, epoch)",
|
||||
"CREATE INDEX `IDX_annotation_org_id_type` ON `annotation` (org_id, type)",
|
||||
"CREATE INDEX `IDX_annotation_org_id_updated` ON `annotation` (org_id, updated)",
|
||||
"CREATE TABLE `annotation_tag` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `annotation_id` INT64 NOT NULL, `tag_id` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_annotation_tag_annotation_id_tag_id` ON `annotation_tag` (annotation_id, tag_id)",
|
||||
"CREATE TABLE `anon_device` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `client_ip` STRING(255) NOT NULL, `created_at` TIMESTAMP NOT NULL, `device_id` STRING(127) NOT NULL, `updated_at` TIMESTAMP NOT NULL, `user_agent` STRING(255) NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_anon_device_updated_at` ON `anon_device` (updated_at)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_anon_device_device_id` ON `anon_device` (device_id)",
|
||||
"CREATE TABLE `api_key` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `name` STRING(190) NOT NULL, `key` STRING(190) NOT NULL, `role` STRING(255) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `expires` INT64, `service_account_id` INT64, `last_used_at` TIMESTAMP, `is_revoked` BOOL DEFAULT (false)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_api_key_org_id` ON `api_key` (org_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_api_key_key` ON `api_key` (key)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_api_key_org_id_name` ON `api_key` (org_id, name)",
|
||||
"CREATE TABLE `autoincrement_sequences` (`name` STRING(128) NOT NULL, `next_value` INT64 NOT NULL) PRIMARY KEY (name)",
|
||||
"CREATE TABLE `builtin_role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `role` STRING(190) NOT NULL, `role_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `org_id` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_builtin_role_org_id` ON `builtin_role` (org_id)",
|
||||
"CREATE INDEX `IDX_builtin_role_role_id` ON `builtin_role` (role_id)",
|
||||
"CREATE INDEX `IDX_builtin_role_role` ON `builtin_role` (role)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_builtin_role_org_id_role_id_role` ON `builtin_role` (org_id, role_id, role)",
|
||||
"CREATE TABLE `cache_data` (`cache_key` STRING(168) NOT NULL, `data` BYTES(MAX) NOT NULL, `expires` INT64 NOT NULL, `created_at` INT64 NOT NULL) PRIMARY KEY (cache_key)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_cache_data_cache_key` ON `cache_data` (cache_key)",
|
||||
"CREATE TABLE `cloud_migration_resource` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40) NOT NULL, `resource_type` STRING(40) NOT NULL, `resource_uid` STRING(255), `status` STRING(20) NOT NULL, `error_string` STRING(MAX), `snapshot_uid` STRING(40) NOT NULL, `name` STRING(MAX), `parent_name` STRING(MAX), `error_code` STRING(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `cloud_migration_session` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40), `auth_token` STRING(MAX), `slug` STRING(MAX) NOT NULL, `stack_id` INT64 NOT NULL, `region_slug` STRING(MAX) NOT NULL, `cluster_slug` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `org_id` INT64 NOT NULL DEFAULT (1)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_cloud_migration_session_uid` ON `cloud_migration_session` (uid)",
|
||||
"CREATE TABLE `cloud_migration_snapshot` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40), `session_uid` STRING(40), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `finished` TIMESTAMP, `upload_url` STRING(MAX), `status` STRING(MAX) NOT NULL, `local_directory` STRING(MAX), `gms_snapshot_uid` STRING(MAX), `encryption_key` STRING(MAX), `error_string` STRING(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_cloud_migration_snapshot_uid` ON `cloud_migration_snapshot` (uid)",
|
||||
"CREATE TABLE `correlation` (`uid` STRING(40) NOT NULL, `org_id` INT64 NOT NULL DEFAULT (0), `source_uid` STRING(40) NOT NULL, `target_uid` STRING(40), `label` STRING(MAX) NOT NULL, `description` STRING(MAX) NOT NULL, `config` STRING(MAX), `provisioned` BOOL NOT NULL DEFAULT (false), `type` STRING(40) NOT NULL DEFAULT ('query')) PRIMARY KEY (uid,org_id,source_uid)",
|
||||
"CREATE INDEX `IDX_correlation_org_id` ON `correlation` (org_id)",
|
||||
"CREATE INDEX `IDX_correlation_source_uid` ON `correlation` (source_uid)",
|
||||
"CREATE INDEX `IDX_correlation_uid` ON `correlation` (uid)",
|
||||
"CREATE TABLE `dashboard` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `slug` STRING(189) NOT NULL, `title` STRING(189) NOT NULL, `data` STRING(MAX) NOT NULL, `org_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `updated_by` INT64, `created_by` INT64, `gnet_id` INT64, `plugin_id` STRING(189), `folder_id` INT64 NOT NULL DEFAULT (0), `is_folder` BOOL NOT NULL DEFAULT (false), `has_acl` BOOL NOT NULL DEFAULT (false), `uid` STRING(40), `is_public` BOOL NOT NULL DEFAULT (false), `deleted` TIMESTAMP, `api_version` STRING(16), `folder_uid` STRING(40)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_deleted` ON `dashboard` (deleted)",
|
||||
"CREATE INDEX `IDX_dashboard_gnet_id` ON `dashboard` (gnet_id)",
|
||||
"CREATE INDEX `IDX_dashboard_is_folder` ON `dashboard` (is_folder)",
|
||||
"CREATE INDEX `IDX_dashboard_org_id_folder_id_title` ON `dashboard` (org_id, folder_id, title)",
|
||||
"CREATE INDEX `IDX_dashboard_org_id_plugin_id` ON `dashboard` (org_id, plugin_id)",
|
||||
"CREATE INDEX `IDX_dashboard_org_id` ON `dashboard` (org_id)",
|
||||
"CREATE INDEX `IDX_dashboard_title` ON `dashboard` (title)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_org_id_uid` ON `dashboard` (org_id, uid)",
|
||||
"CREATE TABLE `dashboard_acl` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `user_id` INT64, `team_id` INT64, `permission` INT64 NOT NULL DEFAULT (4), `role` STRING(20), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_acl_dashboard_id` ON `dashboard_acl` (dashboard_id)",
|
||||
"CREATE INDEX `IDX_dashboard_acl_org_id_role` ON `dashboard_acl` (org_id, role)",
|
||||
"CREATE INDEX `IDX_dashboard_acl_permission` ON `dashboard_acl` (permission)",
|
||||
"CREATE INDEX `IDX_dashboard_acl_team_id` ON `dashboard_acl` (team_id)",
|
||||
"CREATE INDEX `IDX_dashboard_acl_user_id` ON `dashboard_acl` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_acl_dashboard_id_team_id` ON `dashboard_acl` (dashboard_id, team_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_acl_dashboard_id_user_id` ON `dashboard_acl` (dashboard_id, user_id)",
|
||||
"CREATE TABLE `dashboard_provisioning` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64, `name` STRING(150) NOT NULL, `external_id` STRING(MAX) NOT NULL, `updated` INT64 NOT NULL DEFAULT (0), `check_sum` STRING(32)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_provisioning_dashboard_id_name` ON `dashboard_provisioning` (dashboard_id, name)",
|
||||
"CREATE INDEX `IDX_dashboard_provisioning_dashboard_id` ON `dashboard_provisioning` (dashboard_id)",
|
||||
"CREATE TABLE `dashboard_public` (`uid` STRING(40) NOT NULL, `dashboard_uid` STRING(40) NOT NULL, `org_id` INT64 NOT NULL, `time_settings` STRING(MAX), `template_variables` STRING(MAX), `access_token` STRING(32) NOT NULL, `created_by` INT64 NOT NULL, `updated_by` INT64, `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP, `is_enabled` BOOL NOT NULL DEFAULT (false), `annotations_enabled` BOOL NOT NULL DEFAULT (false), `time_selection_enabled` BOOL NOT NULL DEFAULT (false), `share` STRING(64) NOT NULL DEFAULT ('public')) PRIMARY KEY (uid)",
|
||||
"CREATE INDEX `IDX_dashboard_public_config_org_id_dashboard_uid` ON `dashboard_public` (org_id, dashboard_uid)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_public_config_access_token` ON `dashboard_public` (access_token)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_public_config_uid` ON `dashboard_public` (uid)",
|
||||
"CREATE TABLE `dashboard_public_email_share` (`uid` STRING(40) NOT NULL, `public_dashboard_uid` STRING(64) NOT NULL, `recipient` STRING(255) NOT NULL, `type` STRING(64) NOT NULL DEFAULT ('email'), `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP NOT NULL) PRIMARY KEY (uid)",
|
||||
"CREATE TABLE `dashboard_public_magic_link` (`uid` STRING(40) NOT NULL, `token_uuid` STRING(64) NOT NULL, `public_dashboard_uid` STRING(64) NOT NULL, `email` STRING(255) NOT NULL, `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP NOT NULL) PRIMARY KEY (uid)",
|
||||
"CREATE TABLE `dashboard_public_session` (`uid` STRING(40) NOT NULL, `cookie_uuid` STRING(64) NOT NULL, `public_dashboard_uid` STRING(64) NOT NULL, `email` STRING(255) NOT NULL, `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP NOT NULL, `last_seen_at` TIMESTAMP) PRIMARY KEY (uid)",
|
||||
"CREATE TABLE `dashboard_public_usage_by_day` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `public_dashboard_uid` STRING(255) NOT NULL, `day` STRING(40) NOT NULL, `views` INT64 NOT NULL, `queries` INT64 NOT NULL, `errors` INT64 NOT NULL, `load_duration` FLOAT64 NOT NULL, `cached_queries` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `dashboard_snapshot` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(255) NOT NULL, `key` STRING(190) NOT NULL, `delete_key` STRING(190) NOT NULL, `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `external` BOOL NOT NULL, `external_url` STRING(255) NOT NULL, `dashboard` STRING(MAX) NOT NULL, `expires` TIMESTAMP NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `external_delete_url` STRING(255), `dashboard_encrypted` BYTES(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_snapshot_user_id` ON `dashboard_snapshot` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_snapshot_delete_key` ON `dashboard_snapshot` (delete_key)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_snapshot_key` ON `dashboard_snapshot` (key)",
|
||||
"CREATE TABLE `dashboard_tag` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64 NOT NULL, `term` STRING(50) NOT NULL, `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_tag_dashboard_id` ON `dashboard_tag` (dashboard_id)",
|
||||
"CREATE TABLE `dashboard_usage_by_day` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64 NOT NULL, `day` STRING(40) NOT NULL, `views` INT64 NOT NULL, `queries` INT64 NOT NULL, `errors` INT64 NOT NULL, `load_duration` FLOAT64 NOT NULL, `cached_queries` INT64 NOT NULL DEFAULT (0), `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_usage_by_day_dashboard_id` ON `dashboard_usage_by_day` (dashboard_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_usage_by_day_dashboard_id_day` ON `dashboard_usage_by_day` (dashboard_id, day)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_usage_by_day_dashboard_uid_org_id_day` ON `dashboard_usage_by_day` (dashboard_uid, org_id, day)",
|
||||
"CREATE TABLE `dashboard_usage_sums` (`dashboard_id` INT64 NOT NULL, `updated` TIMESTAMP NOT NULL, `views_last_1_days` INT64 NOT NULL, `views_last_7_days` INT64 NOT NULL, `views_last_30_days` INT64 NOT NULL, `views_total` INT64 NOT NULL, `queries_last_1_days` INT64 NOT NULL, `queries_last_7_days` INT64 NOT NULL, `queries_last_30_days` INT64 NOT NULL, `queries_total` INT64 NOT NULL, `errors_last_1_days` INT64 NOT NULL DEFAULT (0), `errors_last_7_days` INT64 NOT NULL DEFAULT (0), `errors_last_30_days` INT64 NOT NULL DEFAULT (0), `errors_total` INT64 NOT NULL DEFAULT (0), `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1)) PRIMARY KEY (dashboard_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_usage_sums_org_id_dashboard_uid` ON `dashboard_usage_sums` (org_id, dashboard_uid)",
|
||||
"CREATE TABLE `dashboard_version` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64 NOT NULL, `parent_version` INT64 NOT NULL, `restored_from` INT64 NOT NULL, `version` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `created_by` INT64 NOT NULL, `message` STRING(MAX) NOT NULL, `data` STRING(MAX), `api_version` STRING(16)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_dashboard_version_dashboard_id` ON `dashboard_version` (dashboard_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_version_dashboard_id_version` ON `dashboard_version` (dashboard_id, version)",
|
||||
"CREATE TABLE `data_keys` (`name` STRING(100) NOT NULL, `active` BOOL NOT NULL, `scope` STRING(30) NOT NULL, `provider` STRING(50) NOT NULL, `encrypted_data` BYTES(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `label` STRING(100)) PRIMARY KEY (name)",
|
||||
"CREATE TABLE `data_source` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `version` INT64 NOT NULL, `type` STRING(255) NOT NULL, `name` STRING(190) NOT NULL, `access` STRING(255) NOT NULL, `url` STRING(255) NOT NULL, `password` STRING(255), `user` STRING(255), `database` STRING(255), `basic_auth` BOOL NOT NULL, `basic_auth_user` STRING(255), `basic_auth_password` STRING(255), `is_default` BOOL NOT NULL, `json_data` STRING(MAX), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `with_credentials` BOOL NOT NULL DEFAULT (false), `secure_json_data` STRING(MAX), `read_only` BOOL, `uid` STRING(40) NOT NULL DEFAULT ('0'), `is_prunable` BOOL DEFAULT (false), `api_version` STRING(20)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_data_source_org_id_is_default` ON `data_source` (org_id, is_default)",
|
||||
"CREATE INDEX `IDX_data_source_org_id` ON `data_source` (org_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_org_id_name` ON `data_source` (org_id, name)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_org_id_uid` ON `data_source` (org_id, uid)",
|
||||
"CREATE TABLE `data_source_acl` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `data_source_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `permission` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_data_source_acl_data_source_id` ON `data_source_acl` (data_source_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_acl_data_source_id_team_id_user_id` ON `data_source_acl` (data_source_id, team_id, user_id)",
|
||||
"CREATE TABLE `data_source_cache` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `data_source_id` INT64 NOT NULL, `enabled` BOOL NOT NULL, `ttl_ms` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `use_default_ttl` BOOL NOT NULL DEFAULT (true), `data_source_uid` STRING(40) NOT NULL DEFAULT ('0'), `ttl_resources_ms` INT64 NOT NULL DEFAULT (300000)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_data_source_cache_data_source_id` ON `data_source_cache` (data_source_id)",
|
||||
"CREATE INDEX `IDX_data_source_cache_data_source_uid` ON `data_source_cache` (data_source_uid)",
|
||||
"CREATE TABLE `data_source_usage_by_day` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `data_source_id` INT64 NOT NULL, `day` STRING(40) NOT NULL, `queries` INT64 NOT NULL, `errors` INT64 NOT NULL, `load_duration_ms` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_data_source_usage_by_day_data_source_id` ON `data_source_usage_by_day` (data_source_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_usage_by_day_data_source_id_day` ON `data_source_usage_by_day` (data_source_id, day)",
|
||||
"CREATE TABLE `entity_event` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `entity_id` STRING(1024) NOT NULL, `event_type` STRING(8) NOT NULL, `created` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `file` (`path` STRING(1024) NOT NULL, `path_hash` STRING(64) NOT NULL, `parent_folder_path_hash` STRING(64) NOT NULL, `contents` BYTES(MAX), `etag` STRING(32) NOT NULL, `cache_control` STRING(128) NOT NULL, `content_disposition` STRING(128) NOT NULL, `updated` TIMESTAMP NOT NULL, `created` TIMESTAMP NOT NULL, `size` INT64 NOT NULL, `mime_type` STRING(255) NOT NULL) PRIMARY KEY (path_hash)",
|
||||
"CREATE INDEX `IDX_file_parent_folder_path_hash` ON `file` (parent_folder_path_hash)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_file_path_hash` ON `file` (path_hash)",
|
||||
"CREATE TABLE `file_meta` (`path_hash` STRING(64) NOT NULL, `key` STRING(191) NOT NULL, `value` STRING(1024) NOT NULL) PRIMARY KEY (path_hash,key)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_file_meta_path_hash_key` ON `file_meta` (path_hash, key)",
|
||||
"CREATE TABLE `folder` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40) NOT NULL, `org_id` INT64 NOT NULL, `title` STRING(189) NOT NULL, `description` STRING(255), `parent_uid` STRING(40), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_folder_org_id_uid` ON `folder` (org_id, uid)",
|
||||
"CREATE TABLE `kv_store` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `namespace` STRING(190) NOT NULL, `key` STRING(190) NOT NULL, `value` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_kv_store_org_id_namespace_key` ON `kv_store` (org_id, namespace, key)",
|
||||
"CREATE TABLE `library_element` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `folder_id` INT64 NOT NULL, `uid` STRING(40) NOT NULL, `name` STRING(150) NOT NULL, `kind` INT64 NOT NULL, `type` STRING(40) NOT NULL, `description` STRING(2048) NOT NULL, `model` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `created_by` INT64 NOT NULL, `updated` TIMESTAMP NOT NULL, `updated_by` INT64 NOT NULL, `version` INT64 NOT NULL, `folder_uid` STRING(40)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_library_element_org_id_folder_id_name_kind` ON `library_element` (org_id, folder_id, name, kind)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_library_element_org_id_folder_uid_name_kind` ON `library_element` (org_id, folder_uid, name, kind)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_library_element_org_id_uid` ON `library_element` (org_id, uid)",
|
||||
"CREATE TABLE `library_element_connection` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `element_id` INT64 NOT NULL, `kind` INT64 NOT NULL, `connection_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `created_by` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_library_element_connection_element_id_kind_connection_id` ON `library_element_connection` (element_id, kind, connection_id)",
|
||||
"CREATE TABLE `license_token` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `token` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `login_attempt` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `username` STRING(190) NOT NULL, `ip_address` STRING(30) NOT NULL, `created` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_login_attempt_username` ON `login_attempt` (username)",
|
||||
"CREATE TABLE `migration_log` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `migration_id` STRING(255) NOT NULL, `sql` STRING(MAX) NOT NULL, `success` BOOL NOT NULL, `error` STRING(MAX) NOT NULL, `timestamp` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `ngalert_configuration` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `alertmanagers` STRING(MAX), `created_at` INT64 NOT NULL, `updated_at` INT64 NOT NULL, `send_alerts_to` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_ngalert_configuration_org_id` ON `ngalert_configuration` (org_id)",
|
||||
"CREATE TABLE `org` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `name` STRING(190) NOT NULL, `address1` STRING(255), `address2` STRING(255), `city` STRING(255), `state` STRING(255), `zip_code` STRING(50), `country` STRING(255), `billing_email` STRING(255), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_org_name` ON `org` (name)",
|
||||
"CREATE TABLE `org_user` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `role` STRING(20) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_org_user_org_id` ON `org_user` (org_id)",
|
||||
"CREATE INDEX `IDX_org_user_user_id` ON `org_user` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_org_user_org_id_user_id` ON `org_user` (org_id, user_id)",
|
||||
"CREATE TABLE `permission` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `role_id` INT64 NOT NULL, `action` STRING(190) NOT NULL, `scope` STRING(190) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `kind` STRING(40) NOT NULL DEFAULT (''), `attribute` STRING(40) NOT NULL DEFAULT (''), `identifier` STRING(40) NOT NULL DEFAULT ('')) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_permission_identifier` ON `permission` (identifier)",
|
||||
"CREATE INDEX `IDX_permission_role_id` ON `permission` (role_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_permission_action_scope_role_id` ON `permission` (action, scope, role_id)",
|
||||
"CREATE TABLE `playlist` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(255) NOT NULL, `interval` STRING(255) NOT NULL, `org_id` INT64 NOT NULL, `created_at` INT64 NOT NULL DEFAULT (0), `updated_at` INT64 NOT NULL DEFAULT (0), `uid` STRING(80) NOT NULL DEFAULT ('0')) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_playlist_org_id_uid` ON `playlist` (org_id, uid)",
|
||||
"CREATE TABLE `playlist_item` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `playlist_id` INT64 NOT NULL, `type` STRING(255) NOT NULL, `value` STRING(MAX) NOT NULL, `title` STRING(MAX) NOT NULL, `order` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `plugin_setting` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL DEFAULT (1), `plugin_id` STRING(190) NOT NULL, `enabled` BOOL NOT NULL, `pinned` BOOL NOT NULL, `json_data` STRING(MAX), `secure_json_data` STRING(MAX), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `plugin_version` STRING(50)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_plugin_setting_org_id_plugin_id` ON `plugin_setting` (org_id, plugin_id)",
|
||||
"CREATE TABLE `preferences` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `version` INT64 NOT NULL, `home_dashboard_id` INT64 NOT NULL, `timezone` STRING(50) NOT NULL, `theme` STRING(20) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `team_id` INT64, `week_start` STRING(10), `json_data` STRING(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_preferences_org_id` ON `preferences` (org_id)",
|
||||
"CREATE INDEX `IDX_preferences_user_id` ON `preferences` (user_id)",
|
||||
"CREATE TABLE `provenance_type` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `record_key` STRING(190) NOT NULL, `record_type` STRING(190) NOT NULL, `provenance` STRING(190) NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_provenance_type_record_type_record_key_org_id` ON `provenance_type` (record_type, record_key, org_id)",
|
||||
"CREATE TABLE `query_history` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40) NOT NULL, `org_id` INT64 NOT NULL, `datasource_uid` STRING(40) NOT NULL, `created_by` INT64, `created_at` INT64 NOT NULL, `comment` STRING(MAX) NOT NULL, `queries` STRING(MAX) NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_query_history_org_id_created_by_datasource_uid` ON `query_history` (org_id, created_by, datasource_uid)",
|
||||
"CREATE TABLE `query_history_details` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `query_history_item_uid` STRING(40) NOT NULL, `datasource_uid` STRING(40) NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `query_history_star` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `query_uid` STRING(40) NOT NULL, `user_id` INT64, `org_id` INT64 NOT NULL DEFAULT (1)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_query_history_star_user_id_query_uid` ON `query_history_star` (user_id, query_uid)",
|
||||
"CREATE TABLE `quota` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64, `user_id` INT64, `target` STRING(190) NOT NULL, `limit` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_quota_org_id_user_id_target` ON `quota` (org_id, user_id, target)",
|
||||
"CREATE TABLE `recording_rules` (`id` STRING(128) NOT NULL, `target_ref_id` STRING(128) NOT NULL, `name` STRING(128) NOT NULL, `description` STRING(MAX) NOT NULL, `org_id` INT64 NOT NULL, `interval` INT64 NOT NULL, `range` INT64 NOT NULL, `active` BOOL NOT NULL DEFAULT (false), `count` BOOL NOT NULL DEFAULT (false), `queries` BYTES(MAX) NOT NULL, `created_at` TIMESTAMP NOT NULL, `prom_name` STRING(128)) PRIMARY KEY (id,target_ref_id)",
|
||||
"CREATE TABLE `remote_write_targets` (`id` STRING(128) NOT NULL, `data_source_uid` STRING(128) NOT NULL, `write_path` STRING(128) NOT NULL, `org_id` INT64 NOT NULL) PRIMARY KEY (id,data_source_uid,write_path)",
|
||||
"CREATE TABLE `report` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `org_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `name` STRING(MAX) NOT NULL, `recipients` STRING(MAX) NOT NULL, `reply_to` STRING(MAX), `message` STRING(MAX), `schedule_frequency` STRING(32) NOT NULL, `schedule_day` STRING(32) NOT NULL, `schedule_hour` INT64 NOT NULL, `schedule_minute` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `schedule_timezone` STRING(50) NOT NULL DEFAULT ('Europe/Stockholm'), `time_from` STRING(255), `time_to` STRING(255), `pdf_landscape` BOOL, `schedule_day_of_month` STRING(32), `pdf_layout` STRING(255), `pdf_orientation` STRING(32), `dashboard_uid` STRING(40), `template_vars` STRING(MAX), `enable_dashboard_url` BOOL, `state` STRING(32), `enable_csv` BOOL, `schedule_start` INT64, `schedule_end` INT64, `schedule_interval_frequency` STRING(32), `schedule_interval_amount` INT64, `schedule_workdays_only` BOOL, `formats` STRING(190) NOT NULL DEFAULT ('[\"pdf\"]'), `scale_factor` INT64 NOT NULL DEFAULT (2), `uid` STRING(40), `pdf_show_template_variables` BOOL NOT NULL DEFAULT (false), `pdf_combine_one_file` BOOL NOT NULL DEFAULT (true), `subject` STRING(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_report_dashboard_id` ON `report` (dashboard_id)",
|
||||
"CREATE INDEX `IDX_report_org_id` ON `report` (org_id)",
|
||||
"CREATE INDEX `IDX_report_user_id` ON `report` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_report_org_id_uid` ON `report` (org_id, uid)",
|
||||
"CREATE TABLE `report_dashboards` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `report_id` INT64 NOT NULL, `dashboard_uid` STRING(40) NOT NULL DEFAULT (''), `report_variables` STRING(MAX), `time_to` STRING(255), `time_from` STRING(255), `created` TIMESTAMP) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_report_dashboards_report_id` ON `report_dashboards` (report_id)",
|
||||
"CREATE TABLE `report_settings` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `org_id` INT64 NOT NULL, `branding_report_logo_url` STRING(MAX), `branding_email_logo_url` STRING(MAX), `branding_email_footer_link` STRING(MAX), `branding_email_footer_text` STRING(MAX), `branding_email_footer_mode` STRING(50), `pdf_theme` STRING(40) NOT NULL DEFAULT ('light'), `embedded_image_theme` STRING(40) NOT NULL DEFAULT ('dark')) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(190) NOT NULL, `description` STRING(MAX), `version` INT64 NOT NULL, `org_id` INT64 NOT NULL, `uid` STRING(40) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `display_name` STRING(190), `group_name` STRING(190), `hidden` BOOL NOT NULL DEFAULT (false)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_role_org_id` ON `role` (org_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_role_org_id_name` ON `role` (org_id, name)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_role_uid` ON `role` (uid)",
|
||||
"CREATE TABLE `secrets` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `namespace` STRING(255) NOT NULL, `type` STRING(255) NOT NULL, `value` STRING(MAX), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `seed_assignment` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `builtin_role` STRING(190) NOT NULL, `role_name` STRING(190), `action` STRING(190), `scope` STRING(190), `origin` STRING(190)) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_seed_assignment_builtin_role_action_scope` ON `seed_assignment` (builtin_role, action, scope)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_seed_assignment_builtin_role_role_name` ON `seed_assignment` (builtin_role, role_name)",
|
||||
"CREATE TABLE `server_lock` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `operation_uid` STRING(100) NOT NULL, `version` INT64 NOT NULL, `last_execution` INT64 NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_server_lock_operation_uid` ON `server_lock` (operation_uid)",
|
||||
"CREATE TABLE `session` (`key` STRING(16) NOT NULL, `data` BYTES(MAX) NOT NULL, `expiry` INT64 NOT NULL) PRIMARY KEY (key)",
|
||||
"CREATE TABLE `setting` (`section` STRING(100) NOT NULL, `key` STRING(100) NOT NULL, `value` STRING(MAX) NOT NULL, `encrypted_value` STRING(MAX)) PRIMARY KEY (section,key)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_setting_section_key` ON `setting` (section, key)",
|
||||
"CREATE TABLE `short_url` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `uid` STRING(40) NOT NULL, `path` STRING(MAX) NOT NULL, `created_by` INT64, `created_at` INT64 NOT NULL, `last_seen_at` INT64) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_short_url_org_id_uid` ON `short_url` (org_id, uid)",
|
||||
"CREATE TABLE `signing_key` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `key_id` STRING(255) NOT NULL, `private_key` STRING(MAX) NOT NULL, `added_at` TIMESTAMP NOT NULL, `expires_at` TIMESTAMP, `alg` STRING(255) NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_signing_key_key_id` ON `signing_key` (key_id)",
|
||||
"CREATE TABLE `sso_setting` (`id` STRING(40) NOT NULL, `provider` STRING(255) NOT NULL, `settings` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `is_deleted` BOOL NOT NULL DEFAULT (false)) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `star` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1), `updated` TIMESTAMP) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_star_user_id_dashboard_id` ON `star` (user_id, dashboard_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_star_user_id_dashboard_uid_org_id` ON `star` (user_id, dashboard_uid, org_id)",
|
||||
"CREATE TABLE `tag` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `key` STRING(100) NOT NULL, `value` STRING(100) NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_tag_key_value` ON `tag` (key, value)",
|
||||
"CREATE TABLE `team` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(190) NOT NULL, `org_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `uid` STRING(40), `email` STRING(190)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_team_org_id` ON `team` (org_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_org_id_name` ON `team` (org_id, name)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_org_id_uid` ON `team` (org_id, uid)",
|
||||
"CREATE TABLE `team_group` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `group_id` STRING(190) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_team_group_group_id` ON `team_group` (group_id)",
|
||||
"CREATE INDEX `IDX_team_group_org_id` ON `team_group` (org_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_group_org_id_team_id_group_id` ON `team_group` (org_id, team_id, group_id)",
|
||||
"CREATE TABLE `team_member` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `external` BOOL, `permission` INT64) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_team_member_org_id` ON `team_member` (org_id)",
|
||||
"CREATE INDEX `IDX_team_member_team_id` ON `team_member` (team_id)",
|
||||
"CREATE INDEX `IDX_team_member_user_id_org_id` ON `team_member` (user_id, org_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_member_org_id_team_id_user_id` ON `team_member` (org_id, team_id, user_id)",
|
||||
"CREATE TABLE `team_role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `role_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_team_role_org_id` ON `team_role` (org_id)",
|
||||
"CREATE INDEX `IDX_team_role_team_id` ON `team_role` (team_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_role_org_id_team_id_role_id` ON `team_role` (org_id, team_id, role_id)",
|
||||
"CREATE TABLE `temp_user` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `version` INT64 NOT NULL, `email` STRING(190) NOT NULL, `name` STRING(255), `role` STRING(20), `code` STRING(190) NOT NULL, `status` STRING(20) NOT NULL, `invited_by_user_id` INT64, `email_sent` BOOL NOT NULL, `email_sent_on` TIMESTAMP, `remote_addr` STRING(255), `created` INT64 NOT NULL DEFAULT (0), `updated` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_temp_user_code` ON `temp_user` (code)",
|
||||
"CREATE INDEX `IDX_temp_user_email` ON `temp_user` (email)",
|
||||
"CREATE INDEX `IDX_temp_user_org_id` ON `temp_user` (org_id)",
|
||||
"CREATE INDEX `IDX_temp_user_status` ON `temp_user` (status)",
|
||||
"CREATE TABLE `test_data` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `metric1` STRING(20), `metric2` STRING(150), `value_big_int` INT64, `value_double` FLOAT64, `value_float` FLOAT64, `value_int` INT64, `time_epoch` INT64 NOT NULL, `time_date_time` TIMESTAMP NOT NULL, `time_time_stamp` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `user` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `login` STRING(190) NOT NULL, `email` STRING(190) NOT NULL, `name` STRING(255), `password` STRING(255), `salt` STRING(50), `rands` STRING(50), `company` STRING(255), `org_id` INT64 NOT NULL, `is_admin` BOOL NOT NULL, `email_verified` BOOL, `theme` STRING(255), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `help_flags1` INT64 NOT NULL DEFAULT (0), `last_seen_at` TIMESTAMP, `is_disabled` BOOL NOT NULL DEFAULT (false), `is_service_account` BOOL DEFAULT (false), `uid` STRING(40), `is_provisioned` BOOL NOT NULL DEFAULT (false)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_user_login_email` ON `user` (login, email)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_email` ON `user` (email)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_login` ON `user` (login)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_uid` ON `user` (uid)",
|
||||
"CREATE TABLE `user_auth` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `auth_module` STRING(190) NOT NULL, `auth_id` STRING(190), `created` TIMESTAMP NOT NULL, `o_auth_access_token` STRING(MAX), `o_auth_refresh_token` STRING(MAX), `o_auth_token_type` STRING(MAX), `o_auth_expiry` TIMESTAMP, `o_auth_id_token` STRING(MAX)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_user_auth_auth_module_auth_id` ON `user_auth` (auth_module, auth_id)",
|
||||
"CREATE INDEX `IDX_user_auth_user_id` ON `user_auth` (user_id)",
|
||||
"CREATE TABLE `user_auth_token` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `auth_token` STRING(100) NOT NULL, `prev_auth_token` STRING(100) NOT NULL, `user_agent` STRING(255) NOT NULL, `client_ip` STRING(255) NOT NULL, `auth_token_seen` BOOL NOT NULL, `seen_at` INT64, `rotated_at` INT64 NOT NULL, `created_at` INT64 NOT NULL, `updated_at` INT64 NOT NULL, `revoked_at` INT64, `external_session_id` INT64) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_user_auth_token_revoked_at` ON `user_auth_token` (revoked_at)",
|
||||
"CREATE INDEX `IDX_user_auth_token_user_id` ON `user_auth_token` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_auth_token_auth_token` ON `user_auth_token` (auth_token)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_auth_token_prev_auth_token` ON `user_auth_token` (prev_auth_token)",
|
||||
"CREATE TABLE `user_dashboard_views` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `viewed` TIMESTAMP NOT NULL, `org_id` INT64, `dashboard_uid` STRING(40)) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_user_dashboard_views_dashboard_id` ON `user_dashboard_views` (dashboard_id)",
|
||||
"CREATE INDEX `IDX_user_dashboard_views_org_id_dashboard_uid` ON `user_dashboard_views` (org_id, dashboard_uid)",
|
||||
"CREATE INDEX `IDX_user_dashboard_views_user_id` ON `user_dashboard_views` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_dashboard_views_user_id_dashboard_id` ON `user_dashboard_views` (user_id, dashboard_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_dashboard_views_user_id_org_id_dashboard_uid` ON `user_dashboard_views` (user_id, org_id, dashboard_uid)",
|
||||
"CREATE TABLE `user_external_session` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_auth_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `auth_module` STRING(190) NOT NULL, `access_token` STRING(MAX), `id_token` STRING(MAX), `refresh_token` STRING(MAX), `session_id` STRING(1024), `session_id_hash` STRING(44), `name_id` STRING(1024), `name_id_hash` STRING(44), `expires_at` TIMESTAMP, `created_at` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE TABLE `user_role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `role_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `group_mapping_uid` STRING(40) DEFAULT ('')) PRIMARY KEY (id)",
|
||||
"CREATE INDEX `IDX_user_role_org_id` ON `user_role` (org_id)",
|
||||
"CREATE INDEX `IDX_user_role_user_id` ON `user_role` (user_id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_role_org_id_user_id_role_id_group_mapping_uid` ON `user_role` (org_id, user_id, role_id, group_mapping_uid)",
|
||||
"CREATE TABLE `user_stats` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `billing_role` STRING(40) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
|
||||
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_stats_user_id` ON `user_stats` (user_id)",
|
||||
"CREATE TABLE resource ( namespace STRING(63), resource_group STRING(190), resource STRING(190), name STRING(253), folder STRING(253), value BYTES(MAX), resource_version TIMESTAMP NOT NULL OPTIONS ( allow_commit_timestamp = true ), previous_resource_version TIMESTAMP, ) PRIMARY KEY (namespace, resource_group, resource, name)",
|
||||
"CREATE TABLE resource_history ( namespace STRING(63), resource_group STRING(190), resource STRING(190), name STRING(253), folder STRING(253), value BYTES(MAX), resource_version TIMESTAMP NOT NULL OPTIONS ( allow_commit_timestamp = true ), previous_resource_version TIMESTAMP, action INT64, ) PRIMARY KEY (namespace, resource_group, resource, name, resource_version DESC)",
|
||||
"CREATE TABLE resource_blob ( uid STRING(36) NOT NULL, resource_key STRING(MAX) NOT NULL, content_type STRING(100), value BYTES(MAX), ) PRIMARY KEY (uid)",
|
||||
"CREATE CHANGE STREAM resource_stream FOR resource"
|
||||
]
|
||||
@@ -1,761 +0,0 @@
|
||||
[
|
||||
"create migration_log table",
|
||||
"create user table",
|
||||
"add unique index user.login",
|
||||
"add unique index user.email",
|
||||
"drop index UQE_user_login - v1",
|
||||
"drop index UQE_user_email - v1",
|
||||
"Rename table user to user_v1 - v1",
|
||||
"create user table v2",
|
||||
"create index UQE_user_login - v2",
|
||||
"create index UQE_user_email - v2",
|
||||
"copy data_source v1 to v2",
|
||||
"Drop old table user_v1",
|
||||
"Add column help_flags1 to user table",
|
||||
"Update user table charset",
|
||||
"Add last_seen_at column to user",
|
||||
"Add missing user data",
|
||||
"Add is_disabled column to user",
|
||||
"Add index user.login/user.email",
|
||||
"Add is_service_account column to user",
|
||||
"Update is_service_account column to nullable",
|
||||
"Add uid column to user",
|
||||
"Update uid column values for users",
|
||||
"Add unique index user_uid",
|
||||
"Add is_provisioned column to user",
|
||||
"update login field with orgid to allow for multiple service accounts with same name across orgs",
|
||||
"update service accounts login field orgid to appear only once",
|
||||
"update login and email fields to lowercase",
|
||||
"update login and email fields to lowercase2",
|
||||
"create temp user table v1-7",
|
||||
"create index IDX_temp_user_email - v1-7",
|
||||
"create index IDX_temp_user_org_id - v1-7",
|
||||
"create index IDX_temp_user_code - v1-7",
|
||||
"create index IDX_temp_user_status - v1-7",
|
||||
"Update temp_user table charset",
|
||||
"drop index IDX_temp_user_email - v1",
|
||||
"drop index IDX_temp_user_org_id - v1",
|
||||
"drop index IDX_temp_user_code - v1",
|
||||
"drop index IDX_temp_user_status - v1",
|
||||
"Rename table temp_user to temp_user_tmp_qwerty - v1",
|
||||
"create temp_user v2",
|
||||
"create index IDX_temp_user_email - v2",
|
||||
"create index IDX_temp_user_org_id - v2",
|
||||
"create index IDX_temp_user_code - v2",
|
||||
"create index IDX_temp_user_status - v2",
|
||||
"copy temp_user v1 to v2",
|
||||
"drop temp_user_tmp_qwerty",
|
||||
"Set created for temp users that will otherwise prematurely expire",
|
||||
"create star table",
|
||||
"add unique index star.user_id_dashboard_id",
|
||||
"Add column dashboard_uid in star",
|
||||
"Add column org_id in star",
|
||||
"Add column updated in star",
|
||||
"add index in star table on dashboard_uid, org_id and user_id columns",
|
||||
"create org table v1",
|
||||
"create index UQE_org_name - v1",
|
||||
"create org_user table v1",
|
||||
"create index IDX_org_user_org_id - v1",
|
||||
"create index UQE_org_user_org_id_user_id - v1",
|
||||
"create index IDX_org_user_user_id - v1",
|
||||
"Update org table charset",
|
||||
"Update org_user table charset",
|
||||
"Migrate all Read Only Viewers to Viewers",
|
||||
"create dashboard table",
|
||||
"add index dashboard.account_id",
|
||||
"add unique index dashboard_account_id_slug",
|
||||
"create dashboard_tag table",
|
||||
"add unique index dashboard_tag.dasboard_id_term",
|
||||
"drop index UQE_dashboard_tag_dashboard_id_term - v1",
|
||||
"Rename table dashboard to dashboard_v1 - v1",
|
||||
"create dashboard v2",
|
||||
"create index IDX_dashboard_org_id - v2",
|
||||
"create index UQE_dashboard_org_id_slug - v2",
|
||||
"copy dashboard v1 to v2",
|
||||
"drop table dashboard_v1",
|
||||
"alter dashboard.data to mediumtext v1",
|
||||
"Add column updated_by in dashboard - v2",
|
||||
"Add column created_by in dashboard - v2",
|
||||
"Add column gnetId in dashboard",
|
||||
"Add index for gnetId in dashboard",
|
||||
"Add column plugin_id in dashboard",
|
||||
"Add index for plugin_id in dashboard",
|
||||
"Add index for dashboard_id in dashboard_tag",
|
||||
"Update dashboard table charset",
|
||||
"Update dashboard_tag table charset",
|
||||
"Add column folder_id in dashboard",
|
||||
"Add column isFolder in dashboard",
|
||||
"Add column has_acl in dashboard",
|
||||
"Add column uid in dashboard",
|
||||
"Update uid column values in dashboard",
|
||||
"Add unique index dashboard_org_id_uid",
|
||||
"Remove unique index org_id_slug",
|
||||
"Update dashboard title length",
|
||||
"Add unique index for dashboard_org_id_title_folder_id",
|
||||
"create dashboard_provisioning",
|
||||
"Rename table dashboard_provisioning to dashboard_provisioning_tmp_qwerty - v1",
|
||||
"create dashboard_provisioning v2",
|
||||
"create index IDX_dashboard_provisioning_dashboard_id - v2",
|
||||
"create index IDX_dashboard_provisioning_dashboard_id_name - v2",
|
||||
"copy dashboard_provisioning v1 to v2",
|
||||
"drop dashboard_provisioning_tmp_qwerty",
|
||||
"Add check_sum column",
|
||||
"Add index for dashboard_title",
|
||||
"delete tags for deleted dashboards",
|
||||
"delete stars for deleted dashboards",
|
||||
"Add index for dashboard_is_folder",
|
||||
"Add isPublic for dashboard",
|
||||
"Add deleted for dashboard",
|
||||
"Add index for deleted",
|
||||
"Add column dashboard_uid in dashboard_tag",
|
||||
"Add column org_id in dashboard_tag",
|
||||
"Add missing dashboard_uid and org_id to dashboard_tag",
|
||||
"Add apiVersion for dashboard",
|
||||
"Add missing dashboard_uid and org_id to star",
|
||||
"create data_source table",
|
||||
"add index data_source.account_id",
|
||||
"add unique index data_source.account_id_name",
|
||||
"drop index IDX_data_source_account_id - v1",
|
||||
"drop index UQE_data_source_account_id_name - v1",
|
||||
"Rename table data_source to data_source_v1 - v1",
|
||||
"create data_source table v2",
|
||||
"create index IDX_data_source_org_id - v2",
|
||||
"create index UQE_data_source_org_id_name - v2",
|
||||
"Drop old table data_source_v1 #2",
|
||||
"Add column with_credentials",
|
||||
"Add secure json data column",
|
||||
"Update data_source table charset",
|
||||
"Update initial version to 1",
|
||||
"Add read_only data column",
|
||||
"Migrate logging ds to loki ds",
|
||||
"Update json_data with nulls",
|
||||
"Add uid column",
|
||||
"Update uid value",
|
||||
"Add unique index datasource_org_id_uid",
|
||||
"add unique index datasource_org_id_is_default",
|
||||
"Add is_prunable column",
|
||||
"Add api_version column",
|
||||
"create api_key table",
|
||||
"add index api_key.account_id",
|
||||
"add index api_key.key",
|
||||
"add index api_key.account_id_name",
|
||||
"drop index IDX_api_key_account_id - v1",
|
||||
"drop index UQE_api_key_key - v1",
|
||||
"drop index UQE_api_key_account_id_name - v1",
|
||||
"Rename table api_key to api_key_v1 - v1",
|
||||
"create api_key table v2",
|
||||
"create index IDX_api_key_org_id - v2",
|
||||
"create index UQE_api_key_key - v2",
|
||||
"create index UQE_api_key_org_id_name - v2",
|
||||
"copy api_key v1 to v2",
|
||||
"Drop old table api_key_v1",
|
||||
"Update api_key table charset",
|
||||
"Add expires to api_key table",
|
||||
"Add service account foreign key",
|
||||
"set service account foreign key to nil if 0",
|
||||
"Add last_used_at to api_key table",
|
||||
"Add is_revoked column to api_key table",
|
||||
"create dashboard_snapshot table v4",
|
||||
"drop table dashboard_snapshot_v4 #1",
|
||||
"create dashboard_snapshot table v5 #2",
|
||||
"create index UQE_dashboard_snapshot_key - v5",
|
||||
"create index UQE_dashboard_snapshot_delete_key - v5",
|
||||
"create index IDX_dashboard_snapshot_user_id - v5",
|
||||
"alter dashboard_snapshot to mediumtext v2",
|
||||
"Update dashboard_snapshot table charset",
|
||||
"Add column external_delete_url to dashboard_snapshots table",
|
||||
"Add encrypted dashboard json column",
|
||||
"Change dashboard_encrypted column to MEDIUMBLOB",
|
||||
"create quota table v1",
|
||||
"create index UQE_quota_org_id_user_id_target - v1",
|
||||
"Update quota table charset",
|
||||
"create plugin_setting table",
|
||||
"create index UQE_plugin_setting_org_id_plugin_id - v1",
|
||||
"Add column plugin_version to plugin_settings",
|
||||
"Update plugin_setting table charset",
|
||||
"update NULL org_id to 1",
|
||||
"make org_id NOT NULL and DEFAULT VALUE 1",
|
||||
"create session table",
|
||||
"Drop old table playlist table",
|
||||
"Drop old table playlist_item table",
|
||||
"create playlist table v2",
|
||||
"create playlist item table v2",
|
||||
"Update playlist table charset",
|
||||
"Update playlist_item table charset",
|
||||
"Add playlist column created_at",
|
||||
"Add playlist column updated_at",
|
||||
"drop preferences table v2",
|
||||
"drop preferences table v3",
|
||||
"create preferences table v3",
|
||||
"Update preferences table charset",
|
||||
"Add column team_id in preferences",
|
||||
"Update team_id column values in preferences",
|
||||
"Add column week_start in preferences",
|
||||
"Add column preferences.json_data",
|
||||
"alter preferences.json_data to mediumtext v1",
|
||||
"Add preferences index org_id",
|
||||
"Add preferences index user_id",
|
||||
"create alert table v1",
|
||||
"add index alert org_id \u0026 id ",
|
||||
"add index alert state",
|
||||
"add index alert dashboard_id",
|
||||
"Create alert_rule_tag table v1",
|
||||
"Add unique index alert_rule_tag.alert_id_tag_id",
|
||||
"drop index UQE_alert_rule_tag_alert_id_tag_id - v1",
|
||||
"Rename table alert_rule_tag to alert_rule_tag_v1 - v1",
|
||||
"Create alert_rule_tag table v2",
|
||||
"create index UQE_alert_rule_tag_alert_id_tag_id - Add unique index alert_rule_tag.alert_id_tag_id V2",
|
||||
"copy alert_rule_tag v1 to v2",
|
||||
"drop table alert_rule_tag_v1",
|
||||
"create alert_notification table v1",
|
||||
"Add column is_default",
|
||||
"Add column frequency",
|
||||
"Add column send_reminder",
|
||||
"Add column disable_resolve_message",
|
||||
"add index alert_notification org_id \u0026 name",
|
||||
"Update alert table charset",
|
||||
"Update alert_notification table charset",
|
||||
"create notification_journal table v1",
|
||||
"add index notification_journal org_id \u0026 alert_id \u0026 notifier_id",
|
||||
"drop alert_notification_journal",
|
||||
"create alert_notification_state table v1",
|
||||
"add index alert_notification_state org_id \u0026 alert_id \u0026 notifier_id",
|
||||
"Add for to alert table",
|
||||
"Add column uid in alert_notification",
|
||||
"Update uid column values in alert_notification",
|
||||
"Add unique index alert_notification_org_id_uid",
|
||||
"Remove unique index org_id_name",
|
||||
"Add column secure_settings in alert_notification",
|
||||
"alter alert.settings to mediumtext",
|
||||
"Add non-unique index alert_notification_state_alert_id",
|
||||
"Add non-unique index alert_rule_tag_alert_id",
|
||||
"Drop old annotation table v4",
|
||||
"create annotation table v5",
|
||||
"add index annotation 0 v3",
|
||||
"add index annotation 1 v3",
|
||||
"add index annotation 2 v3",
|
||||
"add index annotation 3 v3",
|
||||
"add index annotation 4 v3",
|
||||
"Update annotation table charset",
|
||||
"Add column region_id to annotation table",
|
||||
"Drop category_id index",
|
||||
"Add column tags to annotation table",
|
||||
"Create annotation_tag table v2",
|
||||
"Add unique index annotation_tag.annotation_id_tag_id",
|
||||
"drop index UQE_annotation_tag_annotation_id_tag_id - v2",
|
||||
"Rename table annotation_tag to annotation_tag_v2 - v2",
|
||||
"Create annotation_tag table v3",
|
||||
"create index UQE_annotation_tag_annotation_id_tag_id - Add unique index annotation_tag.annotation_id_tag_id V3",
|
||||
"copy annotation_tag v2 to v3",
|
||||
"drop table annotation_tag_v2",
|
||||
"Update alert annotations and set TEXT to empty",
|
||||
"Add created time to annotation table",
|
||||
"Add updated time to annotation table",
|
||||
"Add index for created in annotation table",
|
||||
"Add index for updated in annotation table",
|
||||
"Convert existing annotations from seconds to milliseconds",
|
||||
"Add epoch_end column",
|
||||
"Add index for epoch_end",
|
||||
"Make epoch_end the same as epoch",
|
||||
"Move region to single row",
|
||||
"Remove index org_id_epoch from annotation table",
|
||||
"Remove index org_id_dashboard_id_panel_id_epoch from annotation table",
|
||||
"Add index for org_id_dashboard_id_epoch_end_epoch on annotation table",
|
||||
"Add index for org_id_epoch_end_epoch on annotation table",
|
||||
"Remove index org_id_epoch_epoch_end from annotation table",
|
||||
"Add index for alert_id on annotation table",
|
||||
"Increase tags column to length 4096",
|
||||
"Increase prev_state column to length 40 not null",
|
||||
"Increase new_state column to length 40 not null",
|
||||
"create test_data table",
|
||||
"create dashboard_version table v1",
|
||||
"add index dashboard_version.dashboard_id",
|
||||
"add unique index dashboard_version.dashboard_id and dashboard_version.version",
|
||||
"Set dashboard version to 1 where 0",
|
||||
"save existing dashboard data in dashboard_version table v1",
|
||||
"alter dashboard_version.data to mediumtext v1",
|
||||
"Add apiVersion for dashboard_version",
|
||||
"create team table",
|
||||
"add index team.org_id",
|
||||
"add unique index team_org_id_name",
|
||||
"Add column uid in team",
|
||||
"Update uid column values in team",
|
||||
"Add unique index team_org_id_uid",
|
||||
"create team member table",
|
||||
"add index team_member.org_id",
|
||||
"add unique index team_member_org_id_team_id_user_id",
|
||||
"add index team_member.team_id",
|
||||
"Add column email to team table",
|
||||
"Add column external to team_member table",
|
||||
"Add column permission to team_member table",
|
||||
"add unique index team_member_user_id_org_id",
|
||||
"create dashboard acl table",
|
||||
"add index dashboard_acl_dashboard_id",
|
||||
"add unique index dashboard_acl_dashboard_id_user_id",
|
||||
"add unique index dashboard_acl_dashboard_id_team_id",
|
||||
"add index dashboard_acl_user_id",
|
||||
"add index dashboard_acl_team_id",
|
||||
"add index dashboard_acl_org_id_role",
|
||||
"add index dashboard_permission",
|
||||
"save default acl rules in dashboard_acl table",
|
||||
"delete acl rules for deleted dashboards and folders",
|
||||
"create tag table",
|
||||
"add index tag.key_value",
|
||||
"create login attempt table",
|
||||
"add index login_attempt.username",
|
||||
"drop index IDX_login_attempt_username - v1",
|
||||
"Rename table login_attempt to login_attempt_tmp_qwerty - v1",
|
||||
"create login_attempt v2",
|
||||
"create index IDX_login_attempt_username - v2",
|
||||
"copy login_attempt v1 to v2",
|
||||
"drop login_attempt_tmp_qwerty",
|
||||
"create user auth table",
|
||||
"create index IDX_user_auth_auth_module_auth_id - v1",
|
||||
"alter user_auth.auth_id to length 190",
|
||||
"Add OAuth access token to user_auth",
|
||||
"Add OAuth refresh token to user_auth",
|
||||
"Add OAuth token type to user_auth",
|
||||
"Add OAuth expiry to user_auth",
|
||||
"Add index to user_id column in user_auth",
|
||||
"Add OAuth ID token to user_auth",
|
||||
"create server_lock table",
|
||||
"add index server_lock.operation_uid",
|
||||
"create user auth token table",
|
||||
"add unique index user_auth_token.auth_token",
|
||||
"add unique index user_auth_token.prev_auth_token",
|
||||
"add index user_auth_token.user_id",
|
||||
"Add revoked_at to the user auth token",
|
||||
"add index user_auth_token.revoked_at",
|
||||
"add external_session_id to user_auth_token",
|
||||
"create cache_data table",
|
||||
"add unique index cache_data.cache_key",
|
||||
"create short_url table v1",
|
||||
"add index short_url.org_id-uid",
|
||||
"alter table short_url alter column created_by type to bigint",
|
||||
"delete alert_definition table",
|
||||
"recreate alert_definition table",
|
||||
"add index in alert_definition on org_id and title columns",
|
||||
"add index in alert_definition on org_id and uid columns",
|
||||
"alter alert_definition table data column to mediumtext in mysql",
|
||||
"drop index in alert_definition on org_id and title columns",
|
||||
"drop index in alert_definition on org_id and uid columns",
|
||||
"add unique index in alert_definition on org_id and title columns",
|
||||
"add unique index in alert_definition on org_id and uid columns",
|
||||
"Add column paused in alert_definition",
|
||||
"drop alert_definition table",
|
||||
"delete alert_definition_version table",
|
||||
"recreate alert_definition_version table",
|
||||
"add index in alert_definition_version table on alert_definition_id and version columns",
|
||||
"add index in alert_definition_version table on alert_definition_uid and version columns",
|
||||
"alter alert_definition_version table data column to mediumtext in mysql",
|
||||
"drop alert_definition_version table",
|
||||
"create alert_instance table",
|
||||
"add index in alert_instance table on def_org_id, def_uid and current_state columns",
|
||||
"add index in alert_instance table on def_org_id, current_state columns",
|
||||
"add column current_state_end to alert_instance",
|
||||
"remove index def_org_id, def_uid, current_state on alert_instance",
|
||||
"remove index def_org_id, current_state on alert_instance",
|
||||
"rename def_org_id to rule_org_id in alert_instance",
|
||||
"rename def_uid to rule_uid in alert_instance",
|
||||
"add index rule_org_id, rule_uid, current_state on alert_instance",
|
||||
"add index rule_org_id, current_state on alert_instance",
|
||||
"add current_reason column related to current_state",
|
||||
"add result_fingerprint column to alert_instance",
|
||||
"create alert_rule table",
|
||||
"add index in alert_rule on org_id and title columns",
|
||||
"add index in alert_rule on org_id and uid columns",
|
||||
"add index in alert_rule on org_id, namespace_uid, group_uid columns",
|
||||
"alter alert_rule table data column to mediumtext in mysql",
|
||||
"add column for to alert_rule",
|
||||
"add column annotations to alert_rule",
|
||||
"add column labels to alert_rule",
|
||||
"remove unique index from alert_rule on org_id, title columns",
|
||||
"add index in alert_rule on org_id, namespase_uid and title columns",
|
||||
"add dashboard_uid column to alert_rule",
|
||||
"add panel_id column to alert_rule",
|
||||
"add index in alert_rule on org_id, dashboard_uid and panel_id columns",
|
||||
"add rule_group_idx column to alert_rule",
|
||||
"add is_paused column to alert_rule table",
|
||||
"fix is_paused column for alert_rule table",
|
||||
"create alert_rule_version table",
|
||||
"add index in alert_rule_version table on rule_org_id, rule_uid and version columns",
|
||||
"add index in alert_rule_version table on rule_org_id, rule_namespace_uid and rule_group columns",
|
||||
"alter alert_rule_version table data column to mediumtext in mysql",
|
||||
"add column for to alert_rule_version",
|
||||
"add column annotations to alert_rule_version",
|
||||
"add column labels to alert_rule_version",
|
||||
"add rule_group_idx column to alert_rule_version",
|
||||
"add is_paused column to alert_rule_versions table",
|
||||
"fix is_paused column for alert_rule_version table",
|
||||
"create_alert_configuration_table",
|
||||
"Add column default in alert_configuration",
|
||||
"alert alert_configuration alertmanager_configuration column from TEXT to MEDIUMTEXT if mysql",
|
||||
"add column org_id in alert_configuration",
|
||||
"add index in alert_configuration table on org_id column",
|
||||
"add configuration_hash column to alert_configuration",
|
||||
"create_ngalert_configuration_table",
|
||||
"add index in ngalert_configuration on org_id column",
|
||||
"add column send_alerts_to in ngalert_configuration",
|
||||
"create provenance_type table",
|
||||
"add index to uniquify (record_key, record_type, org_id) columns",
|
||||
"create alert_image table",
|
||||
"add unique index on token to alert_image table",
|
||||
"support longer URLs in alert_image table",
|
||||
"create_alert_configuration_history_table",
|
||||
"drop non-unique orgID index on alert_configuration",
|
||||
"drop unique orgID index on alert_configuration if exists",
|
||||
"extract alertmanager configuration history to separate table",
|
||||
"add unique index on orgID to alert_configuration",
|
||||
"add last_applied column to alert_configuration_history",
|
||||
"create library_element table v1",
|
||||
"add index library_element org_id-folder_id-name-kind",
|
||||
"create library_element_connection table v1",
|
||||
"add index library_element_connection element_id-kind-connection_id",
|
||||
"add unique index library_element org_id_uid",
|
||||
"increase max description length to 2048",
|
||||
"alter library_element model to mediumtext",
|
||||
"add library_element folder uid",
|
||||
"populate library_element folder_uid",
|
||||
"add index library_element org_id-folder_uid-name-kind",
|
||||
"clone move dashboard alerts to unified alerting",
|
||||
"create data_keys table",
|
||||
"create secrets table",
|
||||
"rename data_keys name column to id",
|
||||
"add name column into data_keys",
|
||||
"copy data_keys id column values into name",
|
||||
"rename data_keys name column to label",
|
||||
"rename data_keys id column back to name",
|
||||
"create kv_store table v1",
|
||||
"add index kv_store.org_id-namespace-key",
|
||||
"update dashboard_uid and panel_id from existing annotations",
|
||||
"create permission table",
|
||||
"add unique index permission.role_id",
|
||||
"add unique index role_id_action_scope",
|
||||
"create role table",
|
||||
"add column display_name",
|
||||
"add column group_name",
|
||||
"add index role.org_id",
|
||||
"add unique index role_org_id_name",
|
||||
"add index role_org_id_uid",
|
||||
"create team role table",
|
||||
"add index team_role.org_id",
|
||||
"add unique index team_role_org_id_team_id_role_id",
|
||||
"add index team_role.team_id",
|
||||
"create user role table",
|
||||
"add index user_role.org_id",
|
||||
"add unique index user_role_org_id_user_id_role_id",
|
||||
"add index user_role.user_id",
|
||||
"create builtin role table",
|
||||
"add index builtin_role.role_id",
|
||||
"add index builtin_role.name",
|
||||
"Add column org_id to builtin_role table",
|
||||
"add index builtin_role.org_id",
|
||||
"add unique index builtin_role_org_id_role_id_role",
|
||||
"Remove unique index role_org_id_uid",
|
||||
"add unique index role.uid",
|
||||
"create seed assignment table",
|
||||
"add unique index builtin_role_role_name",
|
||||
"add column hidden to role table",
|
||||
"permission kind migration",
|
||||
"permission attribute migration",
|
||||
"permission identifier migration",
|
||||
"add permission identifier index",
|
||||
"add permission action scope role_id index",
|
||||
"remove permission role_id action scope index",
|
||||
"add group mapping UID column to user_role table",
|
||||
"add user_role org ID, user ID, role ID, group mapping UID index",
|
||||
"remove user_role org ID, user ID, role ID index",
|
||||
"create query_history table v1",
|
||||
"add index query_history.org_id-created_by-datasource_uid",
|
||||
"alter table query_history alter column created_by type to bigint",
|
||||
"create query_history_details table v1",
|
||||
"rbac disabled migrator",
|
||||
"teams permissions migration",
|
||||
"dashboard permissions",
|
||||
"dashboard permissions uid scopes",
|
||||
"drop managed folder create actions",
|
||||
"alerting notification permissions",
|
||||
"create query_history_star table v1",
|
||||
"add index query_history.user_id-query_uid",
|
||||
"add column org_id in query_history_star",
|
||||
"alter table query_history_star_mig column user_id type to bigint",
|
||||
"create correlation table v1",
|
||||
"add index correlations.uid",
|
||||
"add index correlations.source_uid",
|
||||
"add correlation config column",
|
||||
"drop index IDX_correlation_uid - v1",
|
||||
"drop index IDX_correlation_source_uid - v1",
|
||||
"Rename table correlation to correlation_tmp_qwerty - v1",
|
||||
"create correlation v2",
|
||||
"create index IDX_correlation_uid - v2",
|
||||
"create index IDX_correlation_source_uid - v2",
|
||||
"create index IDX_correlation_org_id - v2",
|
||||
"copy correlation v1 to v2",
|
||||
"drop correlation_tmp_qwerty",
|
||||
"add provisioning column",
|
||||
"add type column",
|
||||
"create entity_events table",
|
||||
"create dashboard public config v1",
|
||||
"drop index UQE_dashboard_public_config_uid - v1",
|
||||
"drop index IDX_dashboard_public_config_org_id_dashboard_uid - v1",
|
||||
"Drop old dashboard public config table",
|
||||
"recreate dashboard public config v1",
|
||||
"create index UQE_dashboard_public_config_uid - v1",
|
||||
"create index IDX_dashboard_public_config_org_id_dashboard_uid - v1",
|
||||
"drop index UQE_dashboard_public_config_uid - v2",
|
||||
"drop index IDX_dashboard_public_config_org_id_dashboard_uid - v2",
|
||||
"Drop public config table",
|
||||
"Recreate dashboard public config v2",
|
||||
"create index UQE_dashboard_public_config_uid - v2",
|
||||
"create index IDX_dashboard_public_config_org_id_dashboard_uid - v2",
|
||||
"create index UQE_dashboard_public_config_access_token - v2",
|
||||
"Rename table dashboard_public_config to dashboard_public - v2",
|
||||
"add annotations_enabled column",
|
||||
"add time_selection_enabled column",
|
||||
"delete orphaned public dashboards",
|
||||
"add share column",
|
||||
"backfill empty share column fields with default of public",
|
||||
"create file table",
|
||||
"file table idx: path natural pk",
|
||||
"file table idx: parent_folder_path_hash fast folder retrieval",
|
||||
"create file_meta table",
|
||||
"file table idx: path key",
|
||||
"set path collation in file table",
|
||||
"migrate contents column to mediumblob for MySQL",
|
||||
"managed permissions migration",
|
||||
"managed folder permissions alert actions migration",
|
||||
"RBAC action name migrator",
|
||||
"Add UID column to playlist",
|
||||
"Update uid column values in playlist",
|
||||
"Add index for uid in playlist",
|
||||
"update group index for alert rules",
|
||||
"managed folder permissions alert actions repeated migration",
|
||||
"admin only folder/dashboard permission",
|
||||
"add action column to seed_assignment",
|
||||
"add scope column to seed_assignment",
|
||||
"remove unique index builtin_role_role_name before nullable update",
|
||||
"update seed_assignment role_name column to nullable",
|
||||
"add unique index builtin_role_name back",
|
||||
"add unique index builtin_role_action_scope",
|
||||
"add primary key to seed_assigment",
|
||||
"add origin column to seed_assignment",
|
||||
"add origin to plugin seed_assignment",
|
||||
"prevent seeding OnCall access",
|
||||
"managed folder permissions alert actions repeated fixed migration",
|
||||
"managed folder permissions library panel actions migration",
|
||||
"migrate external alertmanagers to datsourcse",
|
||||
"create folder table",
|
||||
"Add index for parent_uid",
|
||||
"Add unique index for folder.uid and folder.org_id",
|
||||
"Update folder title length",
|
||||
"Add unique index for folder.title and folder.parent_uid",
|
||||
"Remove unique index for folder.title and folder.parent_uid",
|
||||
"Add unique index for title, parent_uid, and org_id",
|
||||
"Sync dashboard and folder table",
|
||||
"Remove ghost folders from the folder table",
|
||||
"Remove unique index UQE_folder_uid_org_id",
|
||||
"Add unique index UQE_folder_org_id_uid",
|
||||
"Remove unique index UQE_folder_title_parent_uid_org_id",
|
||||
"Add unique index UQE_folder_org_id_parent_uid_title",
|
||||
"Remove index IDX_folder_parent_uid_org_id",
|
||||
"Remove unique index UQE_folder_org_id_parent_uid_title",
|
||||
"create anon_device table",
|
||||
"add unique index anon_device.device_id",
|
||||
"add index anon_device.updated_at",
|
||||
"create signing_key table",
|
||||
"add unique index signing_key.key_id",
|
||||
"set legacy alert migration status in kvstore",
|
||||
"migrate record of created folders during legacy migration to kvstore",
|
||||
"Add folder_uid for dashboard",
|
||||
"Populate dashboard folder_uid column",
|
||||
"Add unique index for dashboard_org_id_folder_uid_title",
|
||||
"Delete unique index for dashboard_org_id_folder_id_title",
|
||||
"Delete unique index for dashboard_org_id_folder_uid_title",
|
||||
"Add unique index for dashboard_org_id_folder_uid_title_is_folder",
|
||||
"Restore index for dashboard_org_id_folder_id_title",
|
||||
"Remove unique index for dashboard_org_id_folder_uid_title_is_folder",
|
||||
"create sso_setting table",
|
||||
"copy kvstore migration status to each org",
|
||||
"add back entry for orgid=0 migrated status",
|
||||
"managed dashboard permissions annotation actions migration",
|
||||
"create cloud_migration table v1",
|
||||
"create cloud_migration_run table v1",
|
||||
"add stack_id column",
|
||||
"add region_slug column",
|
||||
"add cluster_slug column",
|
||||
"add migration uid column",
|
||||
"Update uid column values for migration",
|
||||
"Add unique index migration_uid",
|
||||
"add migration run uid column",
|
||||
"Update uid column values for migration run",
|
||||
"Add unique index migration_run_uid",
|
||||
"Rename table cloud_migration to cloud_migration_session_tmp_qwerty - v1",
|
||||
"create cloud_migration_session v2",
|
||||
"create index UQE_cloud_migration_session_uid - v2",
|
||||
"copy cloud_migration_session v1 to v2",
|
||||
"drop cloud_migration_session_tmp_qwerty",
|
||||
"Rename table cloud_migration_run to cloud_migration_snapshot_tmp_qwerty - v1",
|
||||
"create cloud_migration_snapshot v2",
|
||||
"create index UQE_cloud_migration_snapshot_uid - v2",
|
||||
"copy cloud_migration_snapshot v1 to v2",
|
||||
"drop cloud_migration_snapshot_tmp_qwerty",
|
||||
"add snapshot upload_url column",
|
||||
"add snapshot status column",
|
||||
"add snapshot local_directory column",
|
||||
"add snapshot gms_snapshot_uid column",
|
||||
"add snapshot encryption_key column",
|
||||
"add snapshot error_string column",
|
||||
"create cloud_migration_resource table v1",
|
||||
"delete cloud_migration_snapshot.result column",
|
||||
"add cloud_migration_resource.name column",
|
||||
"add cloud_migration_resource.parent_name column",
|
||||
"add cloud_migration_session.org_id column",
|
||||
"add cloud_migration_resource.error_code column",
|
||||
"increase resource_uid column length",
|
||||
"alter kv_store.value to longtext",
|
||||
"add notification_settings column to alert_rule table",
|
||||
"add notification_settings column to alert_rule_version table",
|
||||
"removing scope from alert.instances:read action migration",
|
||||
"managed folder permissions alerting silences actions migration",
|
||||
"add record column to alert_rule table",
|
||||
"add record column to alert_rule_version table",
|
||||
"add resolved_at column to alert_instance table",
|
||||
"add last_sent_at column to alert_instance table",
|
||||
"Enable traceQL streaming for all Tempo datasources",
|
||||
"Add scope to alert.notifications.receivers:read and alert.notifications.receivers.secrets:read",
|
||||
"add metadata column to alert_rule table",
|
||||
"add metadata column to alert_rule_version table",
|
||||
"delete orphaned service account permissions",
|
||||
"adding action set permissions",
|
||||
"create user_external_session table",
|
||||
"increase name_id column length to 1024",
|
||||
"increase session_id column length to 1024",
|
||||
"remove scope from alert.notifications.receivers:create",
|
||||
"add created_by column to alert_rule_version table",
|
||||
"add updated_by column to alert_rule table",
|
||||
"add alert_rule_state table",
|
||||
"add index to alert_rule_state on org_id and rule_uid columns",
|
||||
"add guid column to alert_rule table",
|
||||
"add rule_guid column to alert_rule_version table",
|
||||
"drop index in alert_rule_version table on rule_org_id, rule_uid and version columns",
|
||||
"populate rule guid in alert rule table",
|
||||
"add index in alert_rule_version table on rule_org_id, rule_uid, rule_guid and version columns",
|
||||
"add index in alert_rule_version table on rule_guid and version columns",
|
||||
"add index in alert_rule table on guid columns",
|
||||
"add missing_series_evals_to_resolve column to alert_rule",
|
||||
"add missing_series_evals_to_resolve column to alert_rule_version",
|
||||
"create data_source_usage_by_day table",
|
||||
"create data_source_usage_by_day(data_source_id) index",
|
||||
"create data_source_usage_by_day(data_source_id, day) unique index",
|
||||
"create dashboard_usage_by_day table",
|
||||
"create dashboard_usage_sums table",
|
||||
"create dashboard_usage_by_day(dashboard_id) index",
|
||||
"create dashboard_usage_by_day(dashboard_id, day) index",
|
||||
"add column errors_last_1_days to dashboard_usage_sums",
|
||||
"add column errors_last_7_days to dashboard_usage_sums",
|
||||
"add column errors_last_30_days to dashboard_usage_sums",
|
||||
"add column errors_total to dashboard_usage_sums",
|
||||
"create dashboard_public_usage_by_day table",
|
||||
"add column cached_queries to dashboard_usage_by_day table",
|
||||
"add column cached_queries to dashboard_public_usage_by_day table",
|
||||
"add column dashboard_uid to dashboard_usage_sums",
|
||||
"add column org_id to dashboard_usage_sums",
|
||||
"add column dashboard_uid to dashboard_usage_by_day",
|
||||
"add column org_id to dashboard_usage_by_day",
|
||||
"create dashboard_usage_by_day(dashboard_uid, org_id, day) unique index",
|
||||
"Add missing dashboard_uid and org_id to dashboard_usage_by_day and dashboard_usage_sums",
|
||||
"Add dashboard_usage_sums(org_id, dashboard_uid) index",
|
||||
"create user_dashboard_views table",
|
||||
"add index user_dashboard_views.user_id",
|
||||
"add index user_dashboard_views.dashboard_id",
|
||||
"add unique index user_dashboard_views_user_id_dashboard_id",
|
||||
"add org_id column to user_dashboard_views",
|
||||
"add dashboard_uid column to user_dashboard_views",
|
||||
"add unique index user_dashboard_views_org_id_dashboard_uid",
|
||||
"add unique index user_dashboard_views_org_id_user_id_dashboard_uid",
|
||||
"populate user_dashboard_views.dashboard_uid and user_dashboard_views.org_id from dashboard table",
|
||||
"create user_stats table",
|
||||
"add unique index user_stats(user_id)",
|
||||
"create data_source_cache table",
|
||||
"add index data_source_cache.data_source_id",
|
||||
"add use_default_ttl column",
|
||||
"add data_source_cache.data_source_uid column",
|
||||
"remove abandoned data_source_cache records",
|
||||
"update data_source_cache.data_source_uid value",
|
||||
"add index data_source_cache.data_source_uid",
|
||||
"add data_source_cache.ttl_resources_ms column",
|
||||
"update data_source_cache.ttl_resources_ms to have the same value as ttl_ms",
|
||||
"create data_source_acl table",
|
||||
"add index data_source_acl.data_source_id",
|
||||
"add unique index datasource_acl.unique",
|
||||
"create license_token table",
|
||||
"drop recorded_queries table v14",
|
||||
"drop recording_rules table v14",
|
||||
"create recording_rules table v14",
|
||||
"create remote_write_targets table v1",
|
||||
"Add prom_name to recording_rules table",
|
||||
"ensure remote_write_targets table",
|
||||
"create report config table v1",
|
||||
"Add index report.user_id",
|
||||
"add index to dashboard_id",
|
||||
"add index to org_id",
|
||||
"Add timezone to the report",
|
||||
"Add time_from to the report",
|
||||
"Add time_to to the report",
|
||||
"Add PDF landscape option to the report",
|
||||
"Add monthly day scheduling option to the report",
|
||||
"Add PDF layout option to the report",
|
||||
"Add PDF orientation option to the report",
|
||||
"Update report pdf_orientation from pdf_landscape",
|
||||
"create report settings table",
|
||||
"Add dashboard_uid field to the report",
|
||||
"Add template_vars field to the report",
|
||||
"Add option to include dashboard url in the report",
|
||||
"Add state field to the report",
|
||||
"Add option to add CSV files to the report",
|
||||
"Add scheduling start date",
|
||||
"Add missing schedule_start date for old reports",
|
||||
"Add scheduling end date",
|
||||
"Add schedulinng custom interval frequency",
|
||||
"Add scheduling custom interval amount",
|
||||
"Add workdays only flag to report",
|
||||
"create report dashboards table",
|
||||
"Add index report_dashboards.report_id",
|
||||
"Migrate report fields into report_dashboards",
|
||||
"Add formats option to the report",
|
||||
"Migrate reports with csv enabled",
|
||||
"Migrate ancient reports",
|
||||
"Add created column in report_dashboards",
|
||||
"Add scale_factor to the report",
|
||||
"Alter scale_factor from TINYINT to SMALLINT",
|
||||
"Add uid column to report",
|
||||
"Add unique index reports_org_id_uid",
|
||||
"Add pdf show template variable values to the report",
|
||||
"Add pdf combine in one file",
|
||||
"Add pdf theme to report settings table",
|
||||
"Add email subject to the report",
|
||||
"Populate email subject with report name",
|
||||
"Add embedded image theme to report settings table",
|
||||
"create team group table",
|
||||
"add index team_group.org_id",
|
||||
"add unique index team_group.org_id_team_id_group_id",
|
||||
"add index team_group.group_id",
|
||||
"create settings table",
|
||||
"add unique index settings.section_key",
|
||||
"add setting.encrypted_value",
|
||||
"migrate role names",
|
||||
"rename orgs roles",
|
||||
"remove duplicated org role",
|
||||
"migrate alerting role names",
|
||||
"data source permissions",
|
||||
"data source uid permissions",
|
||||
"rename permissions:delegate scope",
|
||||
"remove invalid managed permissions",
|
||||
"builtin role migration",
|
||||
"seed permissions migration",
|
||||
"managed permissions migration enterprise",
|
||||
"create table dashboard_public_email_share",
|
||||
"create table dashboard_public_magic_link",
|
||||
"create table dashboard_public_session",
|
||||
"add last_seen_at column"
|
||||
]
|
||||
@@ -35,33 +35,6 @@ type ListOptions struct {
|
||||
Limit int64 // maximum number of results to return. 0 means no limit.
|
||||
}
|
||||
|
||||
// BatchOpMode controls the semantics of each operation in a batch
|
||||
type BatchOpMode int
|
||||
|
||||
const (
|
||||
// BatchOpPut performs an upsert: create or update (never fails on key state)
|
||||
BatchOpPut BatchOpMode = iota
|
||||
// BatchOpCreate creates a new key, fails if the key already exists
|
||||
BatchOpCreate
|
||||
// BatchOpUpdate updates an existing key, fails if the key doesn't exist
|
||||
BatchOpUpdate
|
||||
// BatchOpDelete removes a key, idempotent (never fails on key state)
|
||||
BatchOpDelete
|
||||
)
|
||||
|
||||
// BatchOp represents a single operation in an atomic batch
|
||||
type BatchOp struct {
|
||||
Mode BatchOpMode
|
||||
Key string
|
||||
Value []byte // For Put/Create/Update operations, nil for Delete
|
||||
}
|
||||
|
||||
// Maximum limit for batch operations
|
||||
const MaxBatchOps = 20
|
||||
|
||||
// ErrKeyAlreadyExists is returned when BatchOpCreate is used on an existing key
|
||||
var ErrKeyAlreadyExists = errors.New("key already exists")
|
||||
|
||||
type KV interface {
|
||||
// Keys returns all the keys in the store
|
||||
Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error]
|
||||
@@ -87,17 +60,6 @@ type KV interface {
|
||||
// UnixTimestamp returns the current time in seconds since Epoch.
|
||||
// This is used to ensure the server and client are not too far apart in time.
|
||||
UnixTimestamp(ctx context.Context) (int64, error)
|
||||
|
||||
// Batch executes all operations atomically within a single transaction.
|
||||
// If any operation fails, all operations are rolled back.
|
||||
// Operations are executed in order; the batch stops on first failure.
|
||||
//
|
||||
// Operation semantics:
|
||||
// - BatchOpPut: Upsert (create or update), never fails on key state
|
||||
// - BatchOpCreate: Fail with ErrKeyAlreadyExists if key exists
|
||||
// - BatchOpUpdate: Fail with ErrNotFound if key doesn't exist
|
||||
// - BatchOpDelete: Idempotent, never fails on key state
|
||||
Batch(ctx context.Context, section string, ops []BatchOp) error
|
||||
}
|
||||
|
||||
var _ KV = &badgerKV{}
|
||||
@@ -398,69 +360,3 @@ func (k *badgerKV) BatchDelete(ctx context.Context, section string, keys []strin
|
||||
|
||||
return txn.Commit()
|
||||
}
|
||||
|
||||
func (k *badgerKV) Batch(ctx context.Context, section string, ops []BatchOp) error {
|
||||
if k.db.IsClosed() {
|
||||
return fmt.Errorf("database is closed")
|
||||
}
|
||||
|
||||
if section == "" {
|
||||
return fmt.Errorf("section is required")
|
||||
}
|
||||
|
||||
if len(ops) > MaxBatchOps {
|
||||
return fmt.Errorf("too many operations: %d > %d", len(ops), MaxBatchOps)
|
||||
}
|
||||
|
||||
txn := k.db.NewTransaction(true)
|
||||
defer txn.Discard()
|
||||
|
||||
for _, op := range ops {
|
||||
keyWithSection := section + "/" + op.Key
|
||||
|
||||
switch op.Mode {
|
||||
case BatchOpCreate:
|
||||
// Check that key doesn't exist, then set
|
||||
_, err := txn.Get([]byte(keyWithSection))
|
||||
if err == nil {
|
||||
return ErrKeyAlreadyExists
|
||||
}
|
||||
if !errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return err
|
||||
}
|
||||
if err := txn.Set([]byte(keyWithSection), op.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case BatchOpUpdate:
|
||||
// Check that key exists, then set
|
||||
_, err := txn.Get([]byte(keyWithSection))
|
||||
if errors.Is(err, badger.ErrKeyNotFound) {
|
||||
return ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := txn.Set([]byte(keyWithSection), op.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case BatchOpPut:
|
||||
// Upsert: create or update
|
||||
if err := txn.Set([]byte(keyWithSection), op.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case BatchOpDelete:
|
||||
// Idempotent delete - don't error if not found
|
||||
if err := txn.Delete([]byte(keyWithSection)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown operation mode: %d", op.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
return txn.Commit()
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ const (
|
||||
TestKVUnixTimestamp = "unix timestamp"
|
||||
TestKVBatchGet = "batch get operations"
|
||||
TestKVBatchDelete = "batch delete operations"
|
||||
TestKVBatch = "batch operations"
|
||||
)
|
||||
|
||||
// NewKVFunc is a function that creates a new KV instance for testing
|
||||
@@ -70,7 +69,6 @@ func RunKVTest(t *testing.T, newKV NewKVFunc, opts *KVTestOptions) {
|
||||
{TestKVUnixTimestamp, runTestKVUnixTimestamp},
|
||||
{TestKVBatchGet, runTestKVBatchGet},
|
||||
{TestKVBatchDelete, runTestKVBatchDelete},
|
||||
{TestKVBatch, runTestKVBatch},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -803,259 +801,3 @@ func saveKVHelper(t *testing.T, kv resource.KV, ctx context.Context, section, ke
|
||||
err = writer.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func runTestKVBatch(t *testing.T, kv resource.KV, nsPrefix string) {
|
||||
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
|
||||
section := nsPrefix + "-batch"
|
||||
|
||||
t.Run("batch with empty section", func(t *testing.T) {
|
||||
err := kv.Batch(ctx, "", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "section is required")
|
||||
})
|
||||
|
||||
t.Run("batch with empty ops succeeds", func(t *testing.T) {
|
||||
err := kv.Batch(ctx, section, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch put creates new key", func(t *testing.T) {
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpPut, Key: "put-key", Value: []byte("put-value")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the key was created
|
||||
reader, err := kv.Get(ctx, section, "put-key")
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "put-value", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch put updates existing key", func(t *testing.T) {
|
||||
// First create a key
|
||||
saveKVHelper(t, kv, ctx, section, "put-update-key", strings.NewReader("original-value"))
|
||||
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpPut, Key: "put-update-key", Value: []byte("updated-value")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the key was updated
|
||||
reader, err := kv.Get(ctx, section, "put-update-key")
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "updated-value", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch create succeeds for new key", func(t *testing.T) {
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpCreate, Key: "create-new-key", Value: []byte("new-value")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the key was created
|
||||
reader, err := kv.Get(ctx, section, "create-new-key")
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "new-value", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch create fails for existing key", func(t *testing.T) {
|
||||
// First create a key
|
||||
saveKVHelper(t, kv, ctx, section, "create-exists-key", strings.NewReader("existing-value"))
|
||||
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpCreate, Key: "create-exists-key", Value: []byte("new-value")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
assert.ErrorIs(t, err, resource.ErrKeyAlreadyExists)
|
||||
|
||||
// Verify the original value is unchanged
|
||||
reader, err := kv.Get(ctx, section, "create-exists-key")
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "existing-value", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch update succeeds for existing key", func(t *testing.T) {
|
||||
// First create a key
|
||||
saveKVHelper(t, kv, ctx, section, "update-exists-key", strings.NewReader("original-value"))
|
||||
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpUpdate, Key: "update-exists-key", Value: []byte("updated-value")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the key was updated
|
||||
reader, err := kv.Get(ctx, section, "update-exists-key")
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "updated-value", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch update fails for non-existent key", func(t *testing.T) {
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpUpdate, Key: "update-nonexistent-key", Value: []byte("new-value")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
assert.ErrorIs(t, err, resource.ErrNotFound)
|
||||
|
||||
// Verify the key was not created
|
||||
_, err = kv.Get(ctx, section, "update-nonexistent-key")
|
||||
assert.ErrorIs(t, err, resource.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("batch delete removes existing key", func(t *testing.T) {
|
||||
// First create a key
|
||||
saveKVHelper(t, kv, ctx, section, "delete-exists-key", strings.NewReader("to-be-deleted"))
|
||||
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpDelete, Key: "delete-exists-key"},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the key was deleted
|
||||
_, err = kv.Get(ctx, section, "delete-exists-key")
|
||||
assert.ErrorIs(t, err, resource.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("batch delete is idempotent for non-existent key", func(t *testing.T) {
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpDelete, Key: "delete-nonexistent-key"},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err) // Should succeed even though key doesn't exist
|
||||
})
|
||||
|
||||
t.Run("batch multiple operations atomic success", func(t *testing.T) {
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpPut, Key: "multi-key1", Value: []byte("value1")},
|
||||
{Mode: resource.BatchOpPut, Key: "multi-key2", Value: []byte("value2")},
|
||||
{Mode: resource.BatchOpPut, Key: "multi-key3", Value: []byte("value3")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all keys were created
|
||||
for i := 1; i <= 3; i++ {
|
||||
key := fmt.Sprintf("multi-key%d", i)
|
||||
reader, err := kv.Get(ctx, section, key)
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fmt.Sprintf("value%d", i), string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("batch multiple operations atomic rollback on failure", func(t *testing.T) {
|
||||
// First create a key that will cause the batch to fail
|
||||
saveKVHelper(t, kv, ctx, section, "rollback-exists", strings.NewReader("existing"))
|
||||
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpPut, Key: "rollback-new1", Value: []byte("value1")},
|
||||
{Mode: resource.BatchOpCreate, Key: "rollback-exists", Value: []byte("should-fail")}, // This will fail
|
||||
{Mode: resource.BatchOpPut, Key: "rollback-new2", Value: []byte("value2")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
assert.ErrorIs(t, err, resource.ErrKeyAlreadyExists)
|
||||
|
||||
// Verify rollback: the first operation should NOT have persisted
|
||||
_, err = kv.Get(ctx, section, "rollback-new1")
|
||||
assert.ErrorIs(t, err, resource.ErrNotFound)
|
||||
|
||||
// Verify the third operation was not executed
|
||||
_, err = kv.Get(ctx, section, "rollback-new2")
|
||||
assert.ErrorIs(t, err, resource.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("batch mixed operations", func(t *testing.T) {
|
||||
// Setup: create a key to update and one to delete
|
||||
saveKVHelper(t, kv, ctx, section, "mixed-update", strings.NewReader("original"))
|
||||
saveKVHelper(t, kv, ctx, section, "mixed-delete", strings.NewReader("to-delete"))
|
||||
|
||||
ops := []resource.BatchOp{
|
||||
{Mode: resource.BatchOpCreate, Key: "mixed-create", Value: []byte("created")},
|
||||
{Mode: resource.BatchOpUpdate, Key: "mixed-update", Value: []byte("updated")},
|
||||
{Mode: resource.BatchOpDelete, Key: "mixed-delete"},
|
||||
{Mode: resource.BatchOpPut, Key: "mixed-put", Value: []byte("put")},
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify create
|
||||
reader, err := kv.Get(ctx, section, "mixed-create")
|
||||
require.NoError(t, err)
|
||||
value, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "created", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify update
|
||||
reader, err = kv.Get(ctx, section, "mixed-update")
|
||||
require.NoError(t, err)
|
||||
value, err = io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "updated", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify delete
|
||||
_, err = kv.Get(ctx, section, "mixed-delete")
|
||||
assert.ErrorIs(t, err, resource.ErrNotFound)
|
||||
|
||||
// Verify put
|
||||
reader, err = kv.Get(ctx, section, "mixed-put")
|
||||
require.NoError(t, err)
|
||||
value, err = io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "put", string(value))
|
||||
err = reader.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("batch too many operations", func(t *testing.T) {
|
||||
ops := make([]resource.BatchOp, resource.MaxBatchOps+1)
|
||||
for i := range ops {
|
||||
ops[i] = resource.BatchOp{Mode: resource.BatchOpPut, Key: fmt.Sprintf("key-%d", i), Value: []byte("value")}
|
||||
}
|
||||
|
||||
err := kv.Batch(ctx, section, ops)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "too many operations")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import { useEffect, useMemo } from 'react';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, getAppEvents } from '@grafana/runtime';
|
||||
import { DisplayList, iamAPIv0alpha1, useLazyGetDisplayMappingQuery } from 'app/api/clients/iam/v0alpha1';
|
||||
import {
|
||||
API_GROUP as IAM_API_GROUP,
|
||||
API_VERSION as IAM_API_VERSION,
|
||||
DisplayList,
|
||||
iamAPIv0alpha1,
|
||||
useLazyGetDisplayMappingQuery,
|
||||
} from 'app/api/clients/iam/v0alpha1';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import {
|
||||
useDeleteFolderMutation as useDeleteFolderMutationLegacy,
|
||||
@@ -56,6 +62,8 @@ import {
|
||||
ReplaceFolderApiArg,
|
||||
useGetAffectedItemsQuery,
|
||||
FolderInfo,
|
||||
ObjectMeta,
|
||||
OwnerReference,
|
||||
} from './index';
|
||||
|
||||
function getFolderUrl(uid: string, title: string): string {
|
||||
@@ -66,6 +74,10 @@ function getFolderUrl(uid: string, title: string): string {
|
||||
return `${config.appSubUrl}/dashboards/f/${uid}/${slug}`;
|
||||
}
|
||||
|
||||
type CombinedFolder = FolderDTO & {
|
||||
ownerReferences?: OwnerReference[];
|
||||
};
|
||||
|
||||
const combineFolderResponses = (
|
||||
folder: Folder,
|
||||
legacyFolder: FolderDTO,
|
||||
@@ -75,7 +87,7 @@ const combineFolderResponses = (
|
||||
const updatedBy = folder.metadata.annotations?.[AnnoKeyUpdatedBy];
|
||||
const createdBy = folder.metadata.annotations?.[AnnoKeyCreatedBy];
|
||||
|
||||
const newData: FolderDTO = {
|
||||
const newData: CombinedFolder = {
|
||||
canAdmin: legacyFolder.canAdmin,
|
||||
canDelete: legacyFolder.canDelete,
|
||||
canEdit: legacyFolder.canEdit,
|
||||
@@ -84,6 +96,7 @@ const combineFolderResponses = (
|
||||
createdBy: (createdBy && userDisplay?.display[userDisplay?.keys.indexOf(createdBy)]?.displayName) || 'Anonymous',
|
||||
updatedBy: (updatedBy && userDisplay?.display[userDisplay?.keys.indexOf(updatedBy)]?.displayName) || 'Anonymous',
|
||||
...appPlatformFolderToLegacyFolder(folder),
|
||||
ownerReferences: folder.metadata.ownerReferences || [],
|
||||
};
|
||||
|
||||
if (parents.length) {
|
||||
@@ -101,7 +114,7 @@ const combineFolderResponses = (
|
||||
return newData;
|
||||
};
|
||||
|
||||
export async function getFolderByUidFacade(uid: string): Promise<FolderDTO> {
|
||||
export async function getFolderByUidFacade(uid: string) {
|
||||
const isVirtualFolder = uid && [GENERAL_FOLDER_UID, config.sharedWithMeFolderUID].includes(uid);
|
||||
const shouldUseAppPlatformAPI = Boolean(config.featureToggles.foldersAppPlatformAPI);
|
||||
|
||||
@@ -216,7 +229,7 @@ export function useGetFolderQueryFacade(uid?: string) {
|
||||
|
||||
// Stitch together the responses to create a single FolderDTO object so on the outside this behaves as the legacy
|
||||
// api client.
|
||||
let newData: FolderDTO | undefined = undefined;
|
||||
let newData: CombinedFolder | undefined = undefined;
|
||||
if (
|
||||
resultFolder.data &&
|
||||
resultParents.data &&
|
||||
@@ -359,14 +372,36 @@ export function useCreateFolder() {
|
||||
return legacyHook;
|
||||
}
|
||||
|
||||
const createFolderAppPlatform = async (folder: NewFolder) => {
|
||||
const payload: CreateFolderApiArg = {
|
||||
const createFolderAppPlatform = async (payload: NewFolder & { createAsTeamFolder?: boolean; teamUid?: string }) => {
|
||||
const { createAsTeamFolder, teamUid, ...folder } = payload;
|
||||
const slugifiedTitle = kbn.slugifyForUrl(folder.title);
|
||||
|
||||
const metadataName = `team-${slugifiedTitle}`;
|
||||
const partialMetadata: ObjectMeta =
|
||||
createAsTeamFolder && teamUid
|
||||
? {
|
||||
name: metadataName,
|
||||
ownerReferences: [
|
||||
{
|
||||
apiVersion: `${IAM_API_GROUP}/${IAM_API_VERSION}`,
|
||||
kind: 'Team',
|
||||
name: folder.title,
|
||||
uid: teamUid,
|
||||
controller: true,
|
||||
blockOwnerDeletion: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
: { generateName: 'f' };
|
||||
|
||||
const apiPayload: CreateFolderApiArg = {
|
||||
folder: {
|
||||
spec: {
|
||||
title: folder.title,
|
||||
description: 'Testing a description',
|
||||
},
|
||||
metadata: {
|
||||
generateName: 'f',
|
||||
...partialMetadata,
|
||||
annotations: {
|
||||
...(folder.parentUid && { [AnnoKeyFolder]: folder.parentUid }),
|
||||
},
|
||||
@@ -375,7 +410,7 @@ export function useCreateFolder() {
|
||||
},
|
||||
};
|
||||
|
||||
const result = await createFolder(payload);
|
||||
const result = await createFolder(apiPayload);
|
||||
refresh({ childrenOf: folder.parentUid });
|
||||
deletedDashboardsCache.clear();
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable @grafana/i18n/no-untranslated-strings */
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Box, Button, Combobox, ComboboxOption, Divider, Stack, Text } from '@grafana/ui';
|
||||
import { OwnerReference } from 'app/api/clients/folder/v1beta1';
|
||||
import { useListTeamQuery, API_GROUP, API_VERSION } from 'app/api/clients/iam/v0alpha1';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { TeamOwnerReference } from './OwnerReference';
|
||||
import { SupportedResource, useAddOwnerReference, useGetOwnerReferences } from './hooks';
|
||||
|
||||
const TeamSelector = ({ onChange }: { onChange: (ownerRef: OwnerReference) => void }) => {
|
||||
const { data: teams } = useListTeamQuery({});
|
||||
const teamsOptions = teams?.items.map((team) => ({
|
||||
label: team.spec.title,
|
||||
value: team.metadata.name!,
|
||||
}));
|
||||
return (
|
||||
<Combobox
|
||||
options={teamsOptions}
|
||||
onChange={(team: ComboboxOption<string>) => {
|
||||
onChange({
|
||||
apiVersion: `${API_GROUP}/${API_VERSION}`,
|
||||
kind: 'Team',
|
||||
name: team.label,
|
||||
uid: team.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ManageOwnerReferences = ({
|
||||
resource,
|
||||
resourceId,
|
||||
}: {
|
||||
resource: SupportedResource;
|
||||
resourceId: string;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [addingNewReference, setAddingNewReference] = useState(false);
|
||||
const [pendingReference, setPendingReference] = useState<OwnerReference | null>(null);
|
||||
const ownerReferences = useGetOwnerReferences({ resource, resourceId });
|
||||
const [trigger, result] = useAddOwnerReference({ resource, resourceId });
|
||||
|
||||
const addOwnerReference = (ownerReference: OwnerReference) => {
|
||||
trigger(ownerReference);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Text variant="h3">Owned by:</Text>
|
||||
<Box>
|
||||
{ownerReferences
|
||||
.filter((ownerReference) => ownerReference.kind === 'Team')
|
||||
.map((ownerReference) => (
|
||||
<>
|
||||
<TeamOwnerReference key={ownerReference.uid} ownerReference={ownerReference} />
|
||||
<Divider />
|
||||
</>
|
||||
))}
|
||||
</Box>
|
||||
<Box>
|
||||
{addingNewReference && (
|
||||
<Box paddingBottom={2}>
|
||||
<Text variant="h3">Add new owner reference:</Text>
|
||||
<TeamSelector
|
||||
onChange={(ownerReference) => {
|
||||
setPendingReference(ownerReference);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
addOwnerReference(pendingReference);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Divider />
|
||||
</Box>
|
||||
)}
|
||||
<Button onClick={() => setAddingNewReference(true)}>Add new owner reference</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { OwnerReference } from '@grafana/api-clients/rtkq/folder/v1beta1';
|
||||
import { useGetTeamMembersQuery } from '@grafana/api-clients/rtkq/iam/v0alpha1';
|
||||
import { Stack, Text, Avatar, Link, Tooltip } from '@grafana/ui';
|
||||
|
||||
export const getGravatarUrl = (text: string) => {
|
||||
// todo
|
||||
return `avatar/bd38b9ecaf6169ca02b848f60a44cb95`;
|
||||
};
|
||||
|
||||
export const TeamOwnerReference = ({ ownerReference }: { ownerReference: OwnerReference }) => {
|
||||
const { data: teamMembers } = useGetTeamMembersQuery({ name: ownerReference.uid });
|
||||
|
||||
const avatarURL = getGravatarUrl(ownerReference.name);
|
||||
|
||||
const membersTooltip = (
|
||||
<>
|
||||
<Stack gap={1} direction="column">
|
||||
<Text>Team members:</Text>
|
||||
{teamMembers?.items?.map((member) => (
|
||||
<div key={member.identity.name}>
|
||||
<Avatar src={member.avatarURL} /> {member.displayName}
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Link href={`/org/teams/edit/${ownerReference.uid}/members`} key={ownerReference.uid}>
|
||||
<Tooltip content={membersTooltip}>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Avatar src={avatarURL} alt={ownerReference.name} /> {ownerReference.name}
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useReplaceFolderMutation } from '@grafana/api-clients/rtkq/folder/v1beta1';
|
||||
import { folderAPIv1beta1, OwnerReference } from 'app/api/clients/folder/v1beta1';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
const getReferencesEndpointMap = {
|
||||
Folder: (resourceId: string) => folderAPIv1beta1.endpoints.getFolder.initiate({ name: resourceId }),
|
||||
} as const;
|
||||
|
||||
export type SupportedResource = keyof typeof getReferencesEndpointMap;
|
||||
|
||||
export const useGetOwnerReferences = ({
|
||||
resource,
|
||||
resourceId,
|
||||
}: {
|
||||
resource: SupportedResource;
|
||||
resourceId: string;
|
||||
}) => {
|
||||
const [ownerReferences, setOwnerReferences] = useState<OwnerReference[]>([]);
|
||||
const dispatch = useDispatch();
|
||||
const endpointAction = getReferencesEndpointMap[resource];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(endpointAction(resourceId)).then(({ data }) => {
|
||||
if (data?.metadata?.ownerReferences) {
|
||||
setOwnerReferences(data.metadata.ownerReferences);
|
||||
}
|
||||
});
|
||||
}, [dispatch, endpointAction, resourceId]);
|
||||
|
||||
return ownerReferences;
|
||||
};
|
||||
|
||||
export const useAddOwnerReference = ({ resource, resourceId }: { resource: SupportedResource; resourceId: string }) => {
|
||||
const [replaceFolder, result] = useReplaceFolderMutation();
|
||||
return [
|
||||
(ownerReference: OwnerReference) =>
|
||||
replaceFolder({
|
||||
name: resourceId,
|
||||
|
||||
folder: {
|
||||
status: {},
|
||||
metadata: {
|
||||
name: resourceId,
|
||||
ownerReferences: [ownerReference],
|
||||
},
|
||||
spec: {
|
||||
title: resourceId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
result,
|
||||
] as const;
|
||||
};
|
||||
@@ -5,10 +5,12 @@ import { SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Combobox, ComboboxOption, Field, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export type AsyncOptionsLoader = (inputValue: string) => Promise<Array<ComboboxOption<string>>>;
|
||||
|
||||
export interface AlertLabelDropdownProps {
|
||||
onChange: (newValue: SelectableValue<string>) => void;
|
||||
onOpenMenu?: () => void;
|
||||
options: ComboboxOption[];
|
||||
options: ComboboxOption[] | AsyncOptionsLoader;
|
||||
defaultValue?: SelectableValue;
|
||||
type: 'key' | 'value';
|
||||
isLoading?: boolean;
|
||||
@@ -38,7 +40,7 @@ const AlertLabelDropdown: FC<AlertLabelDropdownProps> = forwardRef<HTMLDivElemen
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Field disabled={false} data-testid={`alertlabel-${type}-picker`} className={styles.resetMargin}>
|
||||
<Field noMargin disabled={false} data-testid={`alertlabel-${type}-picker`} className={styles.resetMargin}>
|
||||
<Combobox<string>
|
||||
placeholder={t('alerting.alert-label-dropdown.placeholder-select', 'Choose {{type}}', { type })}
|
||||
width={25}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { OpenAssistantProps, createAssistantContextItem, useAssistant } from '@grafana/assistant';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Menu } from '@grafana/ui';
|
||||
import { GrafanaAlertingRule, GrafanaRecordingRule, GrafanaRule } from 'app/types/unified-alerting';
|
||||
|
||||
@@ -98,8 +98,16 @@ function buildAnalyzeRulePrompt(rule: GrafanaRule): string {
|
||||
function buildAnalyzeAlertingRulePrompt(rule: GrafanaAlertingRule): string {
|
||||
const state = rule.state || 'firing';
|
||||
const timeInfo = rule.activeAt ? ` starting at ${new Date(rule.activeAt).toISOString()}` : '';
|
||||
const alertsNavigationPrompt = config.featureToggles.alertingTriage
|
||||
? '\n- Include navigation to follow up on the alerts page'
|
||||
: '';
|
||||
|
||||
let prompt = `Analyze the ${state} alert "${rule.name}"${timeInfo}.`;
|
||||
let prompt = `
|
||||
Analyze the ${state} alert "${rule.name} (uid: ${rule.uid})"${timeInfo}.
|
||||
- Get the rule definition, read the queries and run them to understand the rule
|
||||
- Get the rule state and instances to understand its current state
|
||||
- Read the rule conditions and understand how it works. Then suggest query and conditions improvements if applicable${alertsNavigationPrompt}
|
||||
`;
|
||||
|
||||
const description = rule.annotations?.description || rule.annotations?.summary || '';
|
||||
if (description) {
|
||||
|
||||
+53
-77
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { Controller, FormProvider, useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { AlertLabels } from '@grafana/alerting/unstable';
|
||||
@@ -13,7 +13,7 @@ import { SupportedPlugin } from '../../../types/pluginBridges';
|
||||
import { KBObjectArray, RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { isPrivateLabelKey } from '../../../utils/labels';
|
||||
import { isRecordingRuleByType } from '../../../utils/rules';
|
||||
import AlertLabelDropdown from '../../AlertLabelDropdown';
|
||||
import AlertLabelDropdown, { AsyncOptionsLoader } from '../../AlertLabelDropdown';
|
||||
import { NeedHelpInfo } from '../NeedHelpInfo';
|
||||
import { useGetLabelsFromDataSourceName } from '../useAlertRuleSuggestions';
|
||||
|
||||
@@ -117,8 +117,7 @@ export function useCombinedLabels(
|
||||
dataSourceName: string,
|
||||
labelsPluginInstalled: boolean,
|
||||
loadingLabelsPlugin: boolean,
|
||||
labelsInSubform: Array<{ key: string; value: string }>,
|
||||
selectedKey: string
|
||||
labelsInSubform: Array<{ key: string; value: string }>
|
||||
) {
|
||||
// ------- Get labels keys and their values from existing alerts
|
||||
const { labels: labelsByKeyFromExisingAlerts, isLoading } = useGetLabelsFromDataSourceName(dataSourceName);
|
||||
@@ -126,18 +125,19 @@ export function useCombinedLabels(
|
||||
const { loading: isLoadingLabels, labelsOpsKeys = [] } = useGetOpsLabelsKeys(
|
||||
!labelsPluginInstalled || loadingLabelsPlugin
|
||||
);
|
||||
//------ Convert the labelsOpsKeys to the same format as the labelsByKeyFromExisingAlerts
|
||||
const labelsByKeyOps = useMemo(() => {
|
||||
return labelsOpsKeys.reduce((acc: Record<string, Set<string>>, label) => {
|
||||
acc[label.name] = new Set();
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Lazy query for fetching label values on demand
|
||||
const [fetchLabelValues] = labelsApi.endpoints.getLabelValues.useLazyQuery();
|
||||
|
||||
//------ Convert the labelsOpsKeys to a Set for quick lookup
|
||||
const opsLabelKeysSet = useMemo(() => {
|
||||
return new Set(labelsOpsKeys.map((label) => label.name));
|
||||
}, [labelsOpsKeys]);
|
||||
|
||||
//------- Convert the keys from the ops labels to options for the dropdown
|
||||
const keysFromGopsLabels = useMemo(() => {
|
||||
return mapLabelsToOptions(Object.keys(labelsByKeyOps).filter(isKeyAllowed), labelsInSubform);
|
||||
}, [labelsByKeyOps, labelsInSubform]);
|
||||
return mapLabelsToOptions(Array.from(opsLabelKeysSet).filter(isKeyAllowed), labelsInSubform);
|
||||
}, [opsLabelKeysSet, labelsInSubform]);
|
||||
|
||||
//------- Convert the keys from the existing alerts to options for the dropdown
|
||||
const keysFromExistingAlerts = useMemo(() => {
|
||||
@@ -158,70 +158,47 @@ export function useCombinedLabels(
|
||||
},
|
||||
];
|
||||
|
||||
const selectedKeyIsFromAlerts = labelsByKeyFromExisingAlerts.has(selectedKey);
|
||||
const selectedKeyIsFromOps = labelsByKeyOps[selectedKey] !== undefined && labelsByKeyOps[selectedKey]?.size > 0;
|
||||
const selectedKeyDoesNotExist = !selectedKeyIsFromAlerts && !selectedKeyIsFromOps;
|
||||
// Create an async options loader for a specific key
|
||||
// This is called by Combobox when the dropdown menu opens
|
||||
const createAsyncValuesLoader = useCallback(
|
||||
(key: string): AsyncOptionsLoader => {
|
||||
return async (_inputValue: string): Promise<Array<ComboboxOption<string>>> => {
|
||||
if (!isKeyAllowed(key) || !key) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const valuesAlreadyFetched = !selectedKeyIsFromAlerts && labelsByKeyOps[selectedKey]?.size > 0;
|
||||
// Collect values from existing alerts first
|
||||
const valuesFromAlerts = labelsByKeyFromExisingAlerts.get(key);
|
||||
const existingValues = valuesFromAlerts ? Array.from(valuesFromAlerts) : [];
|
||||
|
||||
// Only fetch the values for the selected key if it is from ops and the values are not already fetched (the selected key is not in the labelsByKeyOps object)
|
||||
const {
|
||||
currentData: valuesData,
|
||||
isLoading: isLoadingValues = false,
|
||||
error,
|
||||
} = labelsApi.endpoints.getLabelValues.useQuery(
|
||||
{ key: selectedKey },
|
||||
{
|
||||
skip:
|
||||
!labelsPluginInstalled ||
|
||||
!selectedKey ||
|
||||
selectedKeyIsFromAlerts ||
|
||||
valuesAlreadyFetched ||
|
||||
selectedKeyDoesNotExist,
|
||||
}
|
||||
);
|
||||
// Collect values from ops labels (if plugin is installed)
|
||||
let opsValues: string[] = [];
|
||||
if (labelsPluginInstalled && opsLabelKeysSet.has(key)) {
|
||||
try {
|
||||
// RTK Query handles caching automatically
|
||||
const result = await fetchLabelValues({ key }, true).unwrap();
|
||||
if (result?.values?.length) {
|
||||
opsValues = result.values.map((value) => value.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch label values for key:', key, error);
|
||||
}
|
||||
}
|
||||
|
||||
// these are the values for the selected key in case it is from ops
|
||||
const valuesFromSelectedGopsKey = useMemo(() => {
|
||||
// if it is from alerts, we need to fetch the values from the existing alerts
|
||||
if (selectedKeyIsFromAlerts) {
|
||||
return [];
|
||||
}
|
||||
// in case of a label from ops, we need to fetch the values from the plugin
|
||||
// fetch values from ops only if there is no value for the key
|
||||
const valuesForSelectedKey = labelsByKeyOps[selectedKey];
|
||||
const valuesAlreadyFetched = valuesForSelectedKey?.size > 0;
|
||||
if (valuesAlreadyFetched) {
|
||||
return mapLabelsToOptions(valuesForSelectedKey);
|
||||
}
|
||||
if (!isLoadingValues && valuesData?.values?.length && !error) {
|
||||
const values = valuesData?.values.map((value) => value.name);
|
||||
labelsByKeyOps[selectedKey] = new Set(values);
|
||||
return mapLabelsToOptions(values);
|
||||
}
|
||||
return [];
|
||||
}, [selectedKeyIsFromAlerts, labelsByKeyOps, selectedKey, isLoadingValues, valuesData, error]);
|
||||
// Combine: existing values first, then unique ops values (Set preserves first occurrence)
|
||||
const combinedValues = [...new Set([...existingValues, ...opsValues])];
|
||||
|
||||
const getValuesForLabel = useCallback(
|
||||
(key: string) => {
|
||||
if (!isKeyAllowed(key)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// values from existing alerts will take precedence over values from ops
|
||||
if (selectedKeyIsFromAlerts || !labelsPluginInstalled) {
|
||||
return mapLabelsToOptions(labelsByKeyFromExisingAlerts.get(key));
|
||||
}
|
||||
return valuesFromSelectedGopsKey;
|
||||
return mapLabelsToOptions(combinedValues);
|
||||
};
|
||||
},
|
||||
[labelsByKeyFromExisingAlerts, labelsPluginInstalled, valuesFromSelectedGopsKey, selectedKeyIsFromAlerts]
|
||||
[labelsByKeyFromExisingAlerts, labelsPluginInstalled, opsLabelKeysSet, fetchLabelValues]
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading || isLoadingLabels,
|
||||
keysFromExistingAlerts,
|
||||
groupedOptions,
|
||||
getValuesForLabel,
|
||||
createAsyncValuesLoader,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,30 +225,30 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
|
||||
append({ key: '', value: '' });
|
||||
}, [append]);
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState('');
|
||||
// check if the labels plugin is installed
|
||||
const { installed: labelsPluginInstalled = false, loading: loadingLabelsPlugin } = usePluginBridge(
|
||||
SupportedPlugin.Labels
|
||||
);
|
||||
|
||||
const { loading, keysFromExistingAlerts, groupedOptions, getValuesForLabel } = useCombinedLabels(
|
||||
const { loading, keysFromExistingAlerts, groupedOptions, createAsyncValuesLoader } = useCombinedLabels(
|
||||
dataSourceName,
|
||||
labelsPluginInstalled,
|
||||
loadingLabelsPlugin,
|
||||
labelsInSubform,
|
||||
selectedKey
|
||||
labelsInSubform
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2} alignItems="flex-start">
|
||||
{fields.map((field, index) => {
|
||||
// Get the values for this specific row's key directly without memoization
|
||||
// Create an async loader for this specific row's key
|
||||
// This will be called by Combobox when the dropdown opens
|
||||
const currentKey = labelsInSubform[index]?.key || '';
|
||||
const valuesForCurrentKey = getValuesForLabel(currentKey);
|
||||
const asyncValuesLoader = createAsyncValuesLoader(currentKey);
|
||||
|
||||
return (
|
||||
<div key={field.id} className={cx(styles.flexRow, styles.centerAlignRow)} id="hola">
|
||||
<div key={field.id} className={cx(styles.flexRow, styles.centerAlignRow)}>
|
||||
<Field
|
||||
noMargin
|
||||
className={styles.labelInput}
|
||||
invalid={Boolean(errors.labelsInSubform?.[index]?.key?.message)}
|
||||
error={errors.labelsInSubform?.[index]?.key?.message}
|
||||
@@ -295,7 +272,6 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
|
||||
onChange={(newValue: SelectableValue) => {
|
||||
if (newValue) {
|
||||
onChange(newValue.value || newValue.label || '');
|
||||
setSelectedKey(newValue.value);
|
||||
}
|
||||
}}
|
||||
type="key"
|
||||
@@ -306,6 +282,7 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
noMargin
|
||||
className={styles.labelInput}
|
||||
invalid={Boolean(errors.labelsInSubform?.[index]?.value?.message)}
|
||||
error={errors.labelsInSubform?.[index]?.value?.message}
|
||||
@@ -320,16 +297,13 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
|
||||
<AlertLabelDropdown
|
||||
{...rest}
|
||||
defaultValue={value ? { label: value, value: value } : undefined}
|
||||
options={valuesForCurrentKey}
|
||||
options={asyncValuesLoader}
|
||||
isLoading={loading}
|
||||
onChange={(newValue: SelectableValue) => {
|
||||
if (newValue) {
|
||||
onChange(newValue.value || newValue.label || '');
|
||||
}
|
||||
}}
|
||||
onOpenMenu={() => {
|
||||
setSelectedKey(labelsInSubform[index].key);
|
||||
}}
|
||||
type="value"
|
||||
/>
|
||||
);
|
||||
@@ -368,6 +342,7 @@ export const LabelsWithoutSuggestions: FC = () => {
|
||||
<div key={field.id}>
|
||||
<div className={cx(styles.flexRow, styles.centerAlignRow)} data-testid="alertlabel-input-wrapper">
|
||||
<Field
|
||||
noMargin
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.key?.message}
|
||||
error={errors.labels?.[index]?.key?.message}
|
||||
@@ -386,6 +361,7 @@ export const LabelsWithoutSuggestions: FC = () => {
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
noMargin
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.value?.message}
|
||||
error={errors.labels?.[index]?.value?.message}
|
||||
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import * as React from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { render, screen, waitFor, within } from 'test/test-utils';
|
||||
|
||||
import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { mockAlertRuleApi, setupMswServer } from '../../../mockApi';
|
||||
import { getGrafanaRule } from '../../../mocks';
|
||||
import {
|
||||
defaultLabelValues,
|
||||
getLabelValuesHandler,
|
||||
getMockOpsLabels,
|
||||
} from '../../../mocks/server/handlers/plugins/grafana-labels-app';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
|
||||
|
||||
import { LabelsWithSuggestions } from './LabelsField';
|
||||
|
||||
// Existing labels in the form (simulating editing an existing alert rule with ops labels)
|
||||
const existingOpsLabels = getMockOpsLabels();
|
||||
|
||||
const SubFormProviderWrapper = ({
|
||||
children,
|
||||
labels,
|
||||
}: React.PropsWithChildren<{ labels: Array<{ key: string; value: string }> }>) => {
|
||||
const methods = useForm({ defaultValues: { labelsInSubform: labels } });
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
};
|
||||
|
||||
const grafanaRule = getGrafanaRule(undefined, {
|
||||
uid: 'test-rule-uid',
|
||||
title: 'test-alert',
|
||||
namespace_uid: 'folderUID1',
|
||||
data: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasourceUid: 'uid1',
|
||||
queryType: 'alerting',
|
||||
relativeTimeRange: { from: 1000, to: 2000 },
|
||||
model: {
|
||||
refId: 'A',
|
||||
expression: 'vector(1)',
|
||||
queryType: 'alerting',
|
||||
datasource: { uid: 'uid1', type: 'prometheus' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Use the standard MSW server setup which includes all plugin handlers
|
||||
const server = setupMswServer();
|
||||
|
||||
describe('LabelsField with ops labels', () => {
|
||||
beforeEach(() => {
|
||||
// Mock the ruler rules API
|
||||
mockAlertRuleApi(server).rulerRules(GRAFANA_RULES_SOURCE_NAME, {
|
||||
[grafanaRule.namespace.name]: [{ name: grafanaRule.group.name, interval: '1m', rules: [grafanaRule.rulerRule!] }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
clearPluginSettingsCache();
|
||||
});
|
||||
|
||||
async function renderLabelsWithOpsLabels(labels = existingOpsLabels) {
|
||||
const view = render(
|
||||
<SubFormProviderWrapper labels={labels}>
|
||||
<LabelsWithSuggestions dataSourceName="grafana" />
|
||||
</SubFormProviderWrapper>
|
||||
);
|
||||
|
||||
// Wait for the dropdowns to be rendered
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(labels.length);
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
it('should display existing ops labels correctly', async () => {
|
||||
await renderLabelsWithOpsLabels();
|
||||
|
||||
// Verify the keys are displayed
|
||||
expect(screen.getByTestId('labelsInSubform-key-0').querySelector('input')).toHaveValue(existingOpsLabels[0].key);
|
||||
expect(screen.getByTestId('labelsInSubform-key-1').querySelector('input')).toHaveValue(existingOpsLabels[1].key);
|
||||
|
||||
// Verify the values are displayed
|
||||
expect(screen.getByTestId('labelsInSubform-value-0').querySelector('input')).toHaveValue(
|
||||
existingOpsLabels[0].value
|
||||
);
|
||||
expect(screen.getByTestId('labelsInSubform-value-1').querySelector('input')).toHaveValue(
|
||||
existingOpsLabels[1].value
|
||||
);
|
||||
});
|
||||
|
||||
it('should render value dropdowns for each label', async () => {
|
||||
await renderLabelsWithOpsLabels();
|
||||
|
||||
// Verify we have value pickers for each label
|
||||
expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should allow deleting a label', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2);
|
||||
|
||||
await user.click(screen.getByTestId('delete-label-1'));
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(1);
|
||||
expect(screen.getByTestId('labelsInSubform-key-0').querySelector('input')).toHaveValue(existingOpsLabels[0].key);
|
||||
});
|
||||
|
||||
it('should allow adding a new label', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Add more')).toBeVisible());
|
||||
await user.click(screen.getByText('Add more'));
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(3);
|
||||
expect(screen.getByTestId('labelsInSubform-key-2').querySelector('input')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should allow typing custom values in dropdowns', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Add a new label
|
||||
await waitFor(() => expect(screen.getByText('Add more')).toBeVisible());
|
||||
await user.click(screen.getByText('Add more'));
|
||||
|
||||
// Type a custom key and value
|
||||
const newKeyInput = screen.getByTestId('labelsInSubform-key-2').querySelector('input');
|
||||
const newValueInput = screen.getByTestId('labelsInSubform-value-2').querySelector('input');
|
||||
|
||||
await user.type(newKeyInput!, 'customKey{enter}');
|
||||
await user.type(newValueInput!, 'customValue{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('labelsInSubform-key-2').querySelector('input')).toHaveValue('customKey');
|
||||
});
|
||||
expect(screen.getByTestId('labelsInSubform-value-2').querySelector('input')).toHaveValue('customValue');
|
||||
});
|
||||
|
||||
// When editing an existing alert with labels, the value dropdown should open and be interactive
|
||||
it('should allow opening and interacting with existing label value dropdown', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Click on the first label's value dropdown (sentMail) to open it
|
||||
const firstValueDropdown = within(screen.getByTestId('labelsInSubform-value-0'));
|
||||
const combobox = firstValueDropdown.getByRole('combobox');
|
||||
|
||||
// Verify initial value is set
|
||||
expect(combobox).toHaveValue(existingOpsLabels[0].value);
|
||||
|
||||
// Open the dropdown
|
||||
await user.click(combobox);
|
||||
|
||||
// Verify dropdown is open (not showing "No options found" state)
|
||||
expect(combobox).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Close and reopen to verify it remains interactive
|
||||
await user.keyboard('{Escape}');
|
||||
expect(combobox).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
await user.click(combobox);
|
||||
expect(combobox).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
// Test that value dropdowns can be opened and interacted with for different label keys
|
||||
// Note: Dropdown content cannot be verified via text due to Combobox virtualization in JSDOM
|
||||
it('should allow opening value dropdowns for different label keys', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Open the first label's value dropdown (sentMail)
|
||||
const firstValueDropdown = within(screen.getByTestId('labelsInSubform-value-0'));
|
||||
const firstCombobox = firstValueDropdown.getByRole('combobox');
|
||||
await user.click(firstCombobox);
|
||||
|
||||
// Verify dropdown is open
|
||||
expect(firstCombobox).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Close and open second dropdown
|
||||
await user.keyboard('{Escape}');
|
||||
|
||||
// Open the second label's value dropdown (stage)
|
||||
const secondValueDropdown = within(screen.getByTestId('labelsInSubform-value-1'));
|
||||
const secondCombobox = secondValueDropdown.getByRole('combobox');
|
||||
await user.click(secondCombobox);
|
||||
|
||||
// Verify second dropdown is open
|
||||
expect(secondCombobox).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
// Test that after deleting and re-adding a label, the value dropdown can be opened
|
||||
it('should allow opening value dropdown after deleting and re-adding a label', async () => {
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Delete the second label (stage)
|
||||
await user.click(screen.getByTestId('delete-label-1'));
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(1);
|
||||
|
||||
// Add a new label
|
||||
await waitFor(() => expect(screen.getByText('Add more')).toBeVisible());
|
||||
await user.click(screen.getByText('Add more'));
|
||||
|
||||
// Set the new label key to 'team'
|
||||
const newKeyDropdown = within(screen.getByTestId('labelsInSubform-key-1'));
|
||||
await user.type(newKeyDropdown.getByRole('combobox'), 'team{enter}');
|
||||
|
||||
// Verify the key was set
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('labelsInSubform-key-1').querySelector('input')).toHaveValue('team');
|
||||
});
|
||||
|
||||
// Open the new label's value dropdown
|
||||
const newValueDropdown = within(screen.getByTestId('labelsInSubform-value-1'));
|
||||
const combobox = newValueDropdown.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
// Verify dropdown is open
|
||||
expect(combobox).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
// Test that opening the value dropdown requests values for the CORRECT label key
|
||||
// This verifies the async loader is called with the right key
|
||||
it('should request correct label values when opening value dropdown', async () => {
|
||||
const requestedKeys: string[] = [];
|
||||
|
||||
// Add a spy handler that tracks which keys are requested
|
||||
server.use(getLabelValuesHandler(defaultLabelValues, (key) => requestedKeys.push(key)));
|
||||
|
||||
const { user } = await renderLabelsWithOpsLabels();
|
||||
|
||||
// Open the first label's value dropdown (sentMail)
|
||||
const firstValueDropdown = within(screen.getByTestId('labelsInSubform-value-0'));
|
||||
await user.click(firstValueDropdown.getByRole('combobox'));
|
||||
|
||||
// Wait for the API call to be made
|
||||
await waitFor(() => {
|
||||
expect(requestedKeys).toContain('sentMail');
|
||||
});
|
||||
|
||||
// Close dropdown
|
||||
await user.keyboard('{Escape}');
|
||||
|
||||
// Clear the tracked keys
|
||||
requestedKeys.length = 0;
|
||||
|
||||
// Open the second label's value dropdown (stage)
|
||||
const secondValueDropdown = within(screen.getByTestId('labelsInSubform-value-1'));
|
||||
await user.click(secondValueDropdown.getByRole('combobox'));
|
||||
|
||||
// Wait for the API call - should request 'stage', NOT 'sentMail'
|
||||
await waitFor(() => {
|
||||
expect(requestedKeys).toContain('stage');
|
||||
});
|
||||
|
||||
// Verify we didn't request the wrong key (the bug from escalation #19378)
|
||||
expect(requestedKeys).not.toContain('sentMail');
|
||||
});
|
||||
});
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Unit tests for the createAsyncValuesLoader function in useCombinedLabels hook.
|
||||
*
|
||||
* These tests verify that:
|
||||
* 1. Values from existing alerts are shown first
|
||||
* 2. Values from ops labels are shown after existing values
|
||||
* 3. Duplicate values between existing and ops are excluded from ops
|
||||
* 4. The order is: existing values first, then unique ops values
|
||||
*/
|
||||
|
||||
describe('createAsyncValuesLoader logic', () => {
|
||||
// Simulate the data structures used in useCombinedLabels
|
||||
const labelsByKeyFromExistingAlerts = new Map<string, Set<string>>([
|
||||
['severity', new Set(['warning', 'error', 'critical'])],
|
||||
['team', new Set(['frontend', 'backend', 'platform'])],
|
||||
['environment', new Set(['production', 'staging'])],
|
||||
]);
|
||||
|
||||
// Simulate ops labels (from grafana-labels-app plugin)
|
||||
const opsLabelValues: Record<string, string[]> = {
|
||||
severity: ['info', 'warning', 'critical', 'fatal'], // 'warning' and 'critical' overlap with existing
|
||||
team: ['frontend', 'sre', 'devops'], // 'frontend' overlaps with existing
|
||||
environment: ['production', 'staging', 'development', 'testing'], // 'production' and 'staging' overlap
|
||||
cluster: ['us-east-1', 'us-west-2', 'eu-central-1'], // ops-only key
|
||||
};
|
||||
|
||||
const opsLabelKeys = new Set(['severity', 'team', 'environment', 'cluster']);
|
||||
|
||||
const mapLabelsToOptions = (items: string[]) => {
|
||||
return items.map((item) => ({ label: item, value: item }));
|
||||
};
|
||||
|
||||
// This simulates the current implementation of createAsyncValuesLoader
|
||||
const getValuesForLabel = (key: string, labelsPluginInstalled: boolean): Array<{ label: string; value: string }> => {
|
||||
if (!key) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collect values from existing alerts first
|
||||
const valuesFromAlerts = labelsByKeyFromExistingAlerts.get(key);
|
||||
const existingValues = valuesFromAlerts ? Array.from(valuesFromAlerts) : [];
|
||||
|
||||
// Collect values from ops labels (if plugin is installed)
|
||||
let opsValues: string[] = [];
|
||||
if (labelsPluginInstalled && opsLabelKeys.has(key)) {
|
||||
opsValues = opsLabelValues[key] || [];
|
||||
}
|
||||
|
||||
// Combine: existing values first, then unique ops values (Set preserves first occurrence)
|
||||
const combinedValues = [...new Set([...existingValues, ...opsValues])];
|
||||
|
||||
return mapLabelsToOptions(combinedValues);
|
||||
};
|
||||
|
||||
describe('when labels plugin is installed', () => {
|
||||
it('should combine existing and ops values with existing first', () => {
|
||||
const values = getValuesForLabel('severity', true);
|
||||
|
||||
// Existing: warning, error, critical
|
||||
// Ops: info, warning, critical, fatal (warning and critical are duplicates)
|
||||
// Expected: warning, error, critical, info, fatal
|
||||
expect(values).toHaveLength(5);
|
||||
expect(values.map((v) => v.value)).toEqual(['warning', 'error', 'critical', 'info', 'fatal']);
|
||||
});
|
||||
|
||||
it('should exclude duplicate ops values that exist in existing alerts', () => {
|
||||
const values = getValuesForLabel('environment', true);
|
||||
|
||||
// Existing: production, staging
|
||||
// Ops: production, staging, development, testing (production and staging are duplicates)
|
||||
// Expected: production, staging, development, testing
|
||||
expect(values).toHaveLength(4);
|
||||
expect(values.map((v) => v.value)).toEqual(['production', 'staging', 'development', 'testing']);
|
||||
});
|
||||
|
||||
it('should return only ops values for ops-only keys', () => {
|
||||
const values = getValuesForLabel('cluster', true);
|
||||
|
||||
// No existing alerts for 'cluster', only ops values
|
||||
expect(values).toHaveLength(3);
|
||||
expect(values.map((v) => v.value)).toEqual(['us-east-1', 'us-west-2', 'eu-central-1']);
|
||||
});
|
||||
|
||||
it('should return only existing values for keys not in ops', () => {
|
||||
// Add a key that exists in alerts but not in ops
|
||||
labelsByKeyFromExistingAlerts.set('custom', new Set(['value1', 'value2']));
|
||||
|
||||
const values = getValuesForLabel('custom', true);
|
||||
|
||||
expect(values).toHaveLength(2);
|
||||
expect(values.map((v) => v.value)).toEqual(['value1', 'value2']);
|
||||
|
||||
// Cleanup
|
||||
labelsByKeyFromExistingAlerts.delete('custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when labels plugin is NOT installed', () => {
|
||||
it('should return only existing alert values', () => {
|
||||
const values = getValuesForLabel('severity', false);
|
||||
|
||||
// Only existing values, no ops values
|
||||
expect(values).toHaveLength(3);
|
||||
expect(values.map((v) => v.value)).toEqual(['warning', 'error', 'critical']);
|
||||
});
|
||||
|
||||
it('should return empty array for ops-only keys', () => {
|
||||
const values = getValuesForLabel('cluster', false);
|
||||
|
||||
// 'cluster' only exists in ops, not in existing alerts
|
||||
expect(values).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty array for empty key', () => {
|
||||
const values = getValuesForLabel('', true);
|
||||
expect(values).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return empty array for unknown keys', () => {
|
||||
const values = getValuesForLabel('unknown-key', true);
|
||||
expect(values).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should preserve order: existing values first, then unique ops values', () => {
|
||||
const values = getValuesForLabel('team', true);
|
||||
|
||||
// Existing: frontend, backend, platform
|
||||
// Ops: frontend, sre, devops (frontend is duplicate)
|
||||
// Expected order: frontend, backend, platform, sre, devops
|
||||
const valueStrings = values.map((v) => v.value);
|
||||
|
||||
// Check that existing values come before ops values
|
||||
expect(valueStrings.indexOf('frontend')).toBeLessThan(valueStrings.indexOf('sre'));
|
||||
expect(valueStrings.indexOf('backend')).toBeLessThan(valueStrings.indexOf('sre'));
|
||||
expect(valueStrings.indexOf('platform')).toBeLessThan(valueStrings.indexOf('devops'));
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Re-exports all plugin proxy handlers
|
||||
*/
|
||||
import labelsHandlers from './grafana-labels-app';
|
||||
import onCallHandlers from './grafana-oncall';
|
||||
|
||||
/**
|
||||
* Array of all plugin handlers that are required across Alerting tests
|
||||
*/
|
||||
const allPluginProxyHandlers = [...onCallHandlers];
|
||||
const allPluginProxyHandlers = [...onCallHandlers, ...labelsHandlers];
|
||||
|
||||
export default allPluginProxyHandlers;
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { HttpResponse, http } from 'msw';
|
||||
|
||||
import { LabelItem, LabelKeyAndValues } from 'app/features/alerting/unified/api/labelsApi';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
const BASE_URL = `/api/plugins/${SupportedPlugin.Labels}/resources`;
|
||||
|
||||
// Default mock data for ops labels
|
||||
export const defaultLabelKeys: LabelItem[] = [
|
||||
{ id: '1', name: 'sentMail', prescribed: false },
|
||||
{ id: '2', name: 'stage', prescribed: false },
|
||||
{ id: '3', name: 'team', prescribed: false },
|
||||
];
|
||||
|
||||
export const defaultLabelValues: Record<string, LabelItem[]> = {
|
||||
sentMail: [
|
||||
{ id: '1', name: 'true', prescribed: false },
|
||||
{ id: '2', name: 'false', prescribed: false },
|
||||
],
|
||||
stage: [
|
||||
{ id: '1', name: 'production', prescribed: false },
|
||||
{ id: '2', name: 'staging', prescribed: false },
|
||||
{ id: '3', name: 'development', prescribed: false },
|
||||
],
|
||||
team: [
|
||||
{ id: '1', name: 'frontend', prescribed: false },
|
||||
{ id: '2', name: 'backend', prescribed: false },
|
||||
{ id: '3', name: 'platform', prescribed: false },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to generate mock ops labels in the form format (key-value pairs).
|
||||
* @param keys - Array of label key names to include (defaults to first two: sentMail, stage)
|
||||
* @param labelValues - Optional custom label values map
|
||||
* @returns Array of { key, value } objects for use in form tests
|
||||
*/
|
||||
export function getMockOpsLabels(
|
||||
keys: string[] = [defaultLabelKeys[0].name, defaultLabelKeys[1].name],
|
||||
labelValues: Record<string, LabelItem[]> = defaultLabelValues
|
||||
): Array<{ key: string; value: string }> {
|
||||
return keys.map((key) => ({
|
||||
key,
|
||||
value: labelValues[key]?.[0]?.name ?? '',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for GET /api/plugins/grafana-labels-app/resources/v1/labels/keys
|
||||
* Returns all available label keys
|
||||
*/
|
||||
export const getLabelsKeysHandler = (labelKeys: LabelItem[] = defaultLabelKeys) =>
|
||||
http.get(`${BASE_URL}/v1/labels/keys`, () => {
|
||||
return HttpResponse.json(labelKeys);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handler for GET /api/plugins/grafana-labels-app/resources/v1/labels/name/:key
|
||||
* Returns values for a specific label key.
|
||||
* @param labelValues - Custom label values map (defaults to defaultLabelValues)
|
||||
* @param onKeyRequested - Optional callback to spy on which keys are requested (useful for testing)
|
||||
*/
|
||||
export const getLabelValuesHandler = (
|
||||
labelValues: Record<string, LabelItem[]> = defaultLabelValues,
|
||||
onKeyRequested?: (key: string) => void
|
||||
) =>
|
||||
http.get<{ key: string }>(`${BASE_URL}/v1/labels/name/:key`, ({ params }) => {
|
||||
const key = params.key;
|
||||
onKeyRequested?.(key);
|
||||
const values = labelValues[key] || [];
|
||||
const response: LabelKeyAndValues = {
|
||||
labelKey: { id: '1', name: key, prescribed: false },
|
||||
values,
|
||||
};
|
||||
return HttpResponse.json(response);
|
||||
});
|
||||
|
||||
const handlers = [getLabelsKeysHandler(), getLabelValuesHandler()];
|
||||
export default handlers;
|
||||
@@ -82,7 +82,7 @@ export default function RulesFilter({ viewMode, onViewModeChange }: RulesFilterP
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const { searchQuery, updateFilters, setSearchQuery } = useRulesFilter();
|
||||
const { filterState, searchQuery, updateFilters, setSearchQuery } = useRulesFilter();
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const { pluginsFilterEnabled } = usePluginsFilterStatus();
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function RulesFilter({ viewMode, onViewModeChange }: RulesFilterP
|
||||
};
|
||||
|
||||
const handleAdvancedFilters: SubmitHandler<AdvancedFilters> = (values) => {
|
||||
updateFilters(formAdvancedFiltersToRuleFilter(values));
|
||||
updateFilters(formAdvancedFiltersToRuleFilter(values, filterState.freeFormWords));
|
||||
trackFilterButtonApplyClick(values, pluginsFilterEnabled);
|
||||
setIsPopupOpen(false);
|
||||
};
|
||||
|
||||
@@ -5,9 +5,12 @@ import { RulesFilter } from '../../search/rulesSearchParser';
|
||||
|
||||
import { AdvancedFilters } from './types';
|
||||
|
||||
export function formAdvancedFiltersToRuleFilter(values: AdvancedFilters): RulesFilter {
|
||||
export function formAdvancedFiltersToRuleFilter(
|
||||
values: AdvancedFilters,
|
||||
existingFreeFormWords: string[] = []
|
||||
): RulesFilter {
|
||||
return {
|
||||
freeFormWords: [],
|
||||
freeFormWords: existingFreeFormWords,
|
||||
...values,
|
||||
namespace: values.namespace || undefined,
|
||||
groupName: values.groupName || undefined,
|
||||
|
||||
@@ -134,6 +134,29 @@ export const pluginMeta = {
|
||||
module: 'public/plugins/grafana-asserts-app/module.js',
|
||||
baseUrl: 'public/plugins/grafana-asserts-app',
|
||||
} satisfies PluginMeta,
|
||||
[SupportedPlugin.Labels]: {
|
||||
id: SupportedPlugin.Labels,
|
||||
name: 'Labels',
|
||||
type: PluginType.app,
|
||||
enabled: true,
|
||||
info: {
|
||||
author: {
|
||||
name: 'Grafana Labs',
|
||||
url: '',
|
||||
},
|
||||
description: 'Labels management for alerting',
|
||||
links: [],
|
||||
logos: {
|
||||
small: 'public/plugins/grafana-labels-app/img/logo.svg',
|
||||
large: 'public/plugins/grafana-labels-app/img/logo.svg',
|
||||
},
|
||||
screenshots: [],
|
||||
version: 'local-dev',
|
||||
updated: '2024-04-09',
|
||||
},
|
||||
module: 'public/plugins/grafana-labels-app/module.js',
|
||||
baseUrl: 'public/plugins/grafana-labels-app',
|
||||
} satisfies PluginMeta,
|
||||
};
|
||||
|
||||
export const plugins: PluginMeta[] = [
|
||||
@@ -141,6 +164,7 @@ export const plugins: PluginMeta[] = [
|
||||
pluginMeta[SupportedPlugin.Incident],
|
||||
pluginMeta[SupportedPlugin.OnCall],
|
||||
pluginMeta['grafana-asserts-app'],
|
||||
pluginMeta[SupportedPlugin.Labels],
|
||||
];
|
||||
|
||||
export function pluginMetaToPluginConfig(pluginMeta: PluginMeta): AppPluginConfig {
|
||||
|
||||
@@ -6,8 +6,9 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
|
||||
import { LinkButton, FilterInput, useStyles2, Text, Stack, Box, Divider } from '@grafana/ui';
|
||||
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { TeamOwnerReference } from 'app/core/components/OwnerReferences/OwnerReference';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
@@ -146,6 +147,19 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
);
|
||||
};
|
||||
|
||||
const ownerReferences = folderDTO && 'ownerReferences' in folderDTO && (
|
||||
<Box>
|
||||
{folderDTO.ownerReferences
|
||||
?.filter((ref) => ref.kind === 'Team')
|
||||
.map((ref) => (
|
||||
<Stack key={ref.uid} direction="row">
|
||||
<Text>Owned by team:</Text>
|
||||
<TeamOwnerReference ownerReference={ref} />
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="dashboards/browse"
|
||||
@@ -153,7 +167,8 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
onEditTitle={showEditTitle ? onEditTitle : undefined}
|
||||
renderTitle={renderTitle}
|
||||
actions={
|
||||
<>
|
||||
<Stack alignItems="center">
|
||||
{ownerReferences}
|
||||
{config.featureToggles.restoreDashboards && hasAdminRights && (
|
||||
<LinkButton
|
||||
variant="secondary"
|
||||
@@ -173,7 +188,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
isReadOnlyRepo={isReadOnlyRepo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Page.Contents className={styles.pageContents}>
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function listFolders(
|
||||
});
|
||||
}
|
||||
|
||||
return folders.map((item) => ({
|
||||
const result = folders.map((item) => ({
|
||||
kind: 'folder',
|
||||
uid: item.uid,
|
||||
title: item.title,
|
||||
@@ -40,6 +40,20 @@ export async function listFolders(
|
||||
// URLs from the backend come with subUrlPrefix already included, so match that behaviour here
|
||||
url: isSharedWithMe(item.uid) ? undefined : getFolderURL(item.uid),
|
||||
}));
|
||||
|
||||
if (!parentUID) {
|
||||
// result.unshift({
|
||||
// kind: 'folder',
|
||||
// uid: 'teamfolders',
|
||||
// title: 'Team folders',
|
||||
// parentTitle,
|
||||
// parentUID,
|
||||
// managedBy: undefined,
|
||||
// url: undefined,
|
||||
// });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listDashboards(parentUID?: string, page = 1, pageSize = PAGE_SIZE): Promise<DashboardViewItem[]> {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function CheckboxCell({
|
||||
}
|
||||
}
|
||||
|
||||
if (isSharedWithMe(item.uid)) {
|
||||
if (isSharedWithMe(item.uid) || item.uid === 'teamfolders') {
|
||||
return <CheckboxSpacer />;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import InfiniteLoader from 'react-window-infinite-loader';
|
||||
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Avatar, useStyles2 } from '@grafana/ui';
|
||||
import { TeamOwnerReference } from 'app/core/components/OwnerReferences/OwnerReference';
|
||||
import { DashboardViewItem } from 'app/features/search/types';
|
||||
|
||||
import {
|
||||
@@ -102,8 +103,27 @@ export function DashboardsTree({
|
||||
Header: t('browse-dashboards.dashboards-tree.tags-column', 'Tags'),
|
||||
Cell: (props: DashboardsTreeCellProps) => <TagsCell {...props} onTagClick={onTagClick} />,
|
||||
};
|
||||
const ownerReferencesColumn: DashboardsTreeColumn = {
|
||||
id: 'ownerReferences',
|
||||
width: 2,
|
||||
Header: 'Owner',
|
||||
Cell: ({ row: { original: data } }) => {
|
||||
const ownerReferences = data.item.ownerReferences;
|
||||
if (!ownerReferences) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ownerReferences.map((ownerReference) => {
|
||||
return <TeamOwnerReference ownerReference={ownerReference} key={ownerReference.uid} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
const canSelect = canSelectItems(permissions);
|
||||
const columns = [canSelect && checkboxColumn, nameColumn, tagsColumns].filter(isTruthy);
|
||||
const columns = [canSelect && checkboxColumn, nameColumn, ownerReferencesColumn, tagsColumns].filter(isTruthy);
|
||||
|
||||
return columns;
|
||||
}, [onFolderClick, onTagClick, permissions]);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem, Text } from '@grafana/ui';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { Permissions } from 'app/core/components/AccessControl/Permissions';
|
||||
import { ManageOwnerReferences } from 'app/core/components/OwnerReferences/ManageOwnerReferences';
|
||||
import { RepoType } from 'app/features/provisioning/Wizard/types';
|
||||
import { BulkMoveProvisionedResource } from 'app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource';
|
||||
import { DeleteProvisionedFolderForm } from 'app/features/provisioning/components/Folders/DeleteProvisionedFolderForm';
|
||||
@@ -30,6 +31,7 @@ interface Props {
|
||||
export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||
const [showManageOwnersDrawer, setShowManageOwnersDrawer] = useState(false);
|
||||
const [showDeleteProvisionedFolderDrawer, setShowDeleteProvisionedFolderDrawer] = useState(false);
|
||||
const [showMoveProvisionedFolderDrawer, setShowMoveProvisionedFolderDrawer] = useState(false);
|
||||
const [moveFolder] = useMoveFolderMutationFacade();
|
||||
@@ -126,14 +128,18 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
};
|
||||
|
||||
const managePermissionsLabel = t('browse-dashboards.folder-actions-button.manage-permissions', 'Manage permissions');
|
||||
const manageOwnersLabel = t('browse-dashboards.folder-actions-button.manage-folder-owners', 'Manage folder owners');
|
||||
const moveLabel = t('browse-dashboards.folder-actions-button.move', 'Move this folder');
|
||||
const deleteLabel = t('browse-dashboards.folder-actions-button.delete', 'Delete this folder');
|
||||
|
||||
const showManageOwners = canViewPermissions && !isProvisionedFolder;
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{canViewPermissions && !isProvisionedFolder && (
|
||||
<MenuItem onClick={() => setShowPermissionsDrawer(true)} label={managePermissionsLabel} />
|
||||
)}
|
||||
{showManageOwners && <MenuItem onClick={() => setShowManageOwnersDrawer(true)} label={manageOwnersLabel} />}
|
||||
{canMoveFolder && !isReadOnlyRepo && (
|
||||
<MenuItem
|
||||
onClick={isProvisionedFolder ? handleShowMoveProvisionedFolderDrawer : showMoveModal}
|
||||
@@ -180,6 +186,16 @@ export function FolderActionsButton({ folder, repoType, isReadOnlyRepo }: Props)
|
||||
<Permissions resource="folders" resourceId={folder.uid} canSetPermissions={canSetPermissions} />
|
||||
</Drawer>
|
||||
)}
|
||||
{showManageOwnersDrawer && (
|
||||
<Drawer
|
||||
title={t('browse-dashboards.action.manage-permissions-button', 'Manage owners')}
|
||||
subtitle={folder.title}
|
||||
onClose={() => setShowManageOwnersDrawer(false)}
|
||||
size="md"
|
||||
>
|
||||
<ManageOwnerReferences resource="Folder" resourceId={folder.uid} />
|
||||
</Drawer>
|
||||
)}
|
||||
{showDeleteProvisionedFolderDrawer && (
|
||||
<Drawer
|
||||
title={
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
|
||||
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
|
||||
import { createAsyncThunk } from 'app/types/store';
|
||||
@@ -53,6 +54,10 @@ export const refreshParents = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
const hackGetOwnerRefs = async () => {
|
||||
return getBackendSrv().get('/apis/folder.grafana.app/v1beta1/namespaces/default/folders');
|
||||
};
|
||||
|
||||
export const refetchChildren = createAsyncThunk(
|
||||
'browseDashboards/refetchChildren',
|
||||
async ({ parentUID, pageSize }: RefetchChildrenArgs): Promise<RefetchChildrenResult> => {
|
||||
@@ -66,6 +71,7 @@ export const refetchChildren = createAsyncThunk(
|
||||
let fetchKind: DashboardViewItemKind | undefined = 'folder';
|
||||
|
||||
let children = await listFolders(uid, undefined, page, pageSize);
|
||||
|
||||
let lastPageOfKind = children.length < pageSize;
|
||||
|
||||
// If we've loaded all folders, load the first page of dashboards.
|
||||
@@ -136,6 +142,16 @@ export const fetchNextChildrenPage = createAsyncThunk(
|
||||
? await listFolders(uid, undefined, page, pageSize)
|
||||
: await listDashboards(uid, page, pageSize);
|
||||
|
||||
const foldersWithOwnerRefs = await hackGetOwnerRefs();
|
||||
|
||||
children.forEach((child) => {
|
||||
const ownerRefs = foldersWithOwnerRefs.items.find((folder) => folder.metadata.name === child.uid)?.metadata
|
||||
.ownerReferences;
|
||||
if (ownerRefs) {
|
||||
child.ownerReferences = ownerRefs;
|
||||
}
|
||||
});
|
||||
|
||||
let lastPageOfKind = children.length < pageSize;
|
||||
|
||||
// If we've loaded all folders, load the first page of dashboards.
|
||||
|
||||
@@ -183,7 +183,7 @@ export function createFlatTree(
|
||||
|
||||
const items = [thisItem, ...mappedChildren];
|
||||
|
||||
if (isSharedWithMe(thisItem.item.uid)) {
|
||||
if (isSharedWithMe(thisItem.item.uid) || thisItem.item.uid === 'teamfolders') {
|
||||
items.push({
|
||||
item: {
|
||||
kind: 'ui',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormEvent } from 'react';
|
||||
import { FormEvent, useMemo } from 'react';
|
||||
|
||||
import { useListTeamQuery } from '@grafana/api-clients/rtkq/iam/v0alpha1';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Checkbox, Stack, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Checkbox, Stack, RadioButtonGroup, useStyles2, Combobox } from '@grafana/ui';
|
||||
import { SortPicker } from 'app/core/components/Select/SortPicker';
|
||||
import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||
|
||||
@@ -76,10 +77,25 @@ export const ActionRow = ({
|
||||
? [SearchLayout.Folders]
|
||||
: [];
|
||||
|
||||
const teams = useListTeamQuery({});
|
||||
|
||||
const teamOptions = useMemo(() => {
|
||||
return teams.data?.items.map((team) => ({
|
||||
label: team.spec.title,
|
||||
value: team.metadata.name || '',
|
||||
}));
|
||||
}, [teams.data?.items]);
|
||||
return (
|
||||
<Stack justifyContent="space-between" alignItems="center">
|
||||
<Stack gap={2} alignItems="center">
|
||||
<TagFilter isClearable={false} tags={state.tag} tagOptions={getTagOptions} onChange={onTagFilterChange} />
|
||||
<Combobox
|
||||
prefixIcon="user-arrows"
|
||||
onChange={() => {}}
|
||||
placeholder="Filter by owner"
|
||||
options={teamOptions || []}
|
||||
isClearable={false}
|
||||
/>
|
||||
{config.featureToggles.panelTitleSearch && (
|
||||
<Checkbox
|
||||
data-testid="include-panels"
|
||||
@@ -99,6 +115,13 @@ export const ActionRow = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* <div className={styles.checkboxWrapper}>
|
||||
<Checkbox
|
||||
label={t('search.actions.owned-by-me', 'My team folders')}
|
||||
onChange={onStarredFilterChange}
|
||||
value={state.teamFolders}
|
||||
/>
|
||||
</div> */}
|
||||
{state.datasource && (
|
||||
<Button icon="times" variant="secondary" onClick={() => onDatasourceChange(undefined)}>
|
||||
<Trans i18nKey="search.actions.remove-datasource-filter">
|
||||
|
||||
@@ -416,7 +416,7 @@ export function toDashboardResults(rsp: SearchAPIResponse, sort: string): DataFr
|
||||
|
||||
async function loadLocationInfo(): Promise<Record<string, LocationInfo>> {
|
||||
// TODO: use proper pagination for search.
|
||||
const uri = `${searchURI}?type=folders&limit=100000`;
|
||||
const uri = `${searchURI}?type=folder&limit=100000`;
|
||||
const rsp = getBackendSrv()
|
||||
.get<SearchAPIResponse>(uri)
|
||||
.then((rsp) => {
|
||||
|
||||
@@ -60,8 +60,11 @@ export function getIconForKind(kind: string, isOpen?: boolean): IconName {
|
||||
}
|
||||
|
||||
export function getIconForItem(item: DashboardViewItemWithUIItems, isOpen?: boolean): IconName {
|
||||
if (item && isSharedWithMe(item.uid)) {
|
||||
if (item.uid === 'teamfolders') {
|
||||
return 'users-alt';
|
||||
}
|
||||
if (item && isSharedWithMe(item.uid)) {
|
||||
return 'share-alt';
|
||||
} else {
|
||||
return getIconForKind(item.kind, isOpen);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
import { OwnerReference } from '@grafana/api-clients/rtkq/folder/v1beta1';
|
||||
import { WithAccessControlMetadata } from '@grafana/data';
|
||||
|
||||
import { ManagerKind } from '../apiserver/types';
|
||||
@@ -83,6 +84,7 @@ export interface DashboardViewItem {
|
||||
sortMeta?: number | string; // value sorted by
|
||||
sortMetaName?: string; // name of the value being sorted e.g. 'Views'
|
||||
managedBy?: ManagerKind;
|
||||
ownerReferences?: OwnerReference[];
|
||||
}
|
||||
|
||||
export interface SearchAction extends Action {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UserEvent } from '@testing-library/user-event';
|
||||
import { Route, Routes } from 'react-router-dom-v5-compat';
|
||||
import { render, screen, waitFor } from 'test/test-utils';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { MOCK_TEAMS } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
@@ -27,9 +27,15 @@ const setup = async () => {
|
||||
return view;
|
||||
};
|
||||
|
||||
const attemptCreateTeam = async (user: UserEvent, teamName?: string, teamEmail?: string) => {
|
||||
const attemptCreateTeam = async (
|
||||
user: UserEvent,
|
||||
teamName?: string,
|
||||
teamEmail?: string,
|
||||
createTeamFolder?: boolean
|
||||
) => {
|
||||
teamName && (await user.type(screen.getByRole('textbox', { name: /name/i }), teamName));
|
||||
teamEmail && (await user.type(screen.getByLabelText(/email/i), teamEmail));
|
||||
createTeamFolder && (await user.click(screen.getByLabelText(/auto-create a team folder/i)));
|
||||
await user.click(screen.getByRole('button', { name: /create/i }));
|
||||
};
|
||||
|
||||
@@ -72,4 +78,22 @@ describe('Create team', () => {
|
||||
|
||||
expect(screen.queryByText(/edit team page/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('team folders enabled', () => {
|
||||
const originalFeatureToggles = config.featureToggles;
|
||||
beforeEach(() => {
|
||||
config.featureToggles = { ...originalFeatureToggles, teamFolders: true };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.featureToggles = originalFeatureToggles;
|
||||
});
|
||||
|
||||
it('renders team folder checkbox', async () => {
|
||||
const { user } = await setup();
|
||||
await attemptCreateTeam(user, MOCK_TEAMS[0].spec.title, undefined, true);
|
||||
|
||||
expect(screen.queryByText(/edit team page/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Button, Field, Input, FieldSet, Stack } from '@grafana/ui';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { Button, Field, Input, FieldSet, Stack, Checkbox, Alert } from '@grafana/ui';
|
||||
import { extractErrorMessage } from 'app/api/utils';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker';
|
||||
@@ -16,34 +16,42 @@ import { TeamDTO } from 'app/types/teams';
|
||||
|
||||
import { useCreateTeam } from './hooks';
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
icon: 'users-alt',
|
||||
id: 'team-new',
|
||||
text: 'New team',
|
||||
subTitle: 'Create a new team. Teams let you grant permissions to a group of users.',
|
||||
};
|
||||
type NewTeamForm = TeamDTO & { createTeamFolder?: boolean };
|
||||
|
||||
const CreateTeam = (): JSX.Element => {
|
||||
export const CreateTeam = (): JSX.Element => {
|
||||
const pageNav: NavModelItem = {
|
||||
icon: 'users-alt',
|
||||
id: 'team-new',
|
||||
text: t('teams.create-team.page-title', 'New team'),
|
||||
subTitle: t(
|
||||
'teams.create-team.page-subtitle',
|
||||
'Create a new team. Teams let you grant permissions to a group of users.'
|
||||
),
|
||||
};
|
||||
|
||||
const teamFoldersEnabled = config.featureToggles.teamFolders;
|
||||
const showRolesPicker = contextSrv.licensedAccessControlEnabled();
|
||||
const currentOrgId = contextSrv.user.orgId;
|
||||
|
||||
const notifyApp = useAppNotification();
|
||||
const [createTeamTrigger] = useCreateTeam();
|
||||
const [createTeamTrigger, createResponse] = useCreateTeam();
|
||||
const [pendingRoles, setPendingRoles] = useState<Role[]>([]);
|
||||
const [{ roleOptions }] = useRoleOptions(currentOrgId);
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useForm<TeamDTO>();
|
||||
} = useForm<NewTeamForm>();
|
||||
|
||||
const createTeam = async (formModel: TeamDTO) => {
|
||||
const createTeam = async (formModel: NewTeamForm) => {
|
||||
try {
|
||||
const { data, error } = await createTeamTrigger(
|
||||
{
|
||||
email: formModel.email || '',
|
||||
name: formModel.name,
|
||||
},
|
||||
pendingRoles
|
||||
pendingRoles,
|
||||
formModel.createTeamFolder
|
||||
);
|
||||
|
||||
const errorMessage = error ? extractErrorMessage(error) : undefined;
|
||||
@@ -73,11 +81,11 @@ const CreateTeam = (): JSX.Element => {
|
||||
label={t('teams.create-team.label-name', 'Name')}
|
||||
required
|
||||
invalid={!!errors.name}
|
||||
error="Team name is required"
|
||||
error={t('teams.create-team.error-name-required', 'Team name is required')}
|
||||
>
|
||||
<Input {...register('name', { required: true })} id="team-name" />
|
||||
</Field>
|
||||
{contextSrv.licensedAccessControlEnabled() && (
|
||||
{showRolesPicker && (
|
||||
<Field noMargin label={t('teams.create-team.label-role', 'Role')}>
|
||||
<TeamRolePicker
|
||||
teamId={0}
|
||||
@@ -106,8 +114,37 @@ const CreateTeam = (): JSX.Element => {
|
||||
placeholder="email@test.com"
|
||||
/>
|
||||
</Field>
|
||||
{teamFoldersEnabled && (
|
||||
<Field
|
||||
noMargin
|
||||
label={t('teams.create-team.team-folder', 'Team folder')}
|
||||
description={t(
|
||||
'teams.create-team.description-team-folder',
|
||||
'This creates a folder associated with the team, where users can add resources like dashboards and schedules with the right permissions.'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
{...register('createTeamFolder')}
|
||||
id="team-folder"
|
||||
label={t(
|
||||
'teams.create-team.team-folder-label-autocreate-a-team-folder',
|
||||
'Auto-create a team folder'
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</Stack>
|
||||
</FieldSet>
|
||||
{Boolean(createResponse.error) && (
|
||||
<Alert title={t('teams.create-team.error-title', 'Error creating team')} severity="error">
|
||||
<Trans i18nKey="teams.create-team.error-message">
|
||||
We were unable to create your new team. Please try again later or contact support.
|
||||
</Trans>
|
||||
<br />
|
||||
<br />
|
||||
<div>{extractErrorMessage(createResponse.error)}</div>
|
||||
</Alert>
|
||||
)}
|
||||
<Button type="submit" variant="primary">
|
||||
<Trans i18nKey="teams.create-team.create">Create</Trans>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useListFolderQuery } from '@grafana/api-clients/rtkq/folder/v1beta1';
|
||||
import { Stack, Text, Link, Icon } from '@grafana/ui';
|
||||
import { Team } from 'app/types/teams';
|
||||
|
||||
export const OwnedResources = ({ team }: { team: Team }) => {
|
||||
const { data } = useListFolderQuery({});
|
||||
const ownedFolders = data?.items.filter((folder) =>
|
||||
folder.metadata.ownerReferences?.some((ref) => ref.uid === team.uid)
|
||||
);
|
||||
return (
|
||||
<Stack gap={1} direction="column">
|
||||
<Text variant="h3">Owned folders:</Text>
|
||||
{ownedFolders &&
|
||||
ownedFolders.map((folder) => (
|
||||
<div key={folder.metadata.uid}>
|
||||
<Link href={`/dashboards/f/${folder.metadata.name}`}>
|
||||
<Icon name="folder" /> <Text>{folder.spec.title}</Text>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
import { StoreState, useSelector } from 'app/types/store';
|
||||
|
||||
import { OwnedResources } from './OwnedResources';
|
||||
import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync';
|
||||
import TeamPermissions from './TeamPermissions';
|
||||
import TeamSettings from './TeamSettings';
|
||||
@@ -26,9 +27,10 @@ enum PageTypes {
|
||||
Members = 'members',
|
||||
Settings = 'settings',
|
||||
GroupSync = 'groupsync',
|
||||
Resources = 'resources',
|
||||
}
|
||||
|
||||
const PAGES = ['members', 'settings', 'groupsync'];
|
||||
const PAGES = ['members', 'settings', 'groupsync', 'resources'];
|
||||
|
||||
const pageNavSelector = createSelector(
|
||||
[
|
||||
@@ -59,24 +61,30 @@ const TeamPages = memo(() => {
|
||||
const renderPage = () => {
|
||||
const currentPage = PAGES.includes(pageName) ? pageName : PAGES[0];
|
||||
|
||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, team!);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, team);
|
||||
const canReadTeamPermissions = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsRead,
|
||||
team!
|
||||
team
|
||||
);
|
||||
const canWriteTeamPermissions = contextSrv.hasPermissionInMetadata(
|
||||
AccessControlAction.ActionTeamsPermissionsWrite,
|
||||
team!
|
||||
team
|
||||
);
|
||||
|
||||
switch (currentPage) {
|
||||
case PageTypes.Members:
|
||||
if (canReadTeamPermissions) {
|
||||
return <TeamPermissions team={team!} />;
|
||||
return <TeamPermissions team={team} />;
|
||||
}
|
||||
return null;
|
||||
case PageTypes.Settings:
|
||||
return canReadTeam && <TeamSettings team={team!} />;
|
||||
return canReadTeam && <TeamSettings team={team} />;
|
||||
case PageTypes.Resources:
|
||||
return canReadTeam && <OwnedResources team={team} />;
|
||||
case PageTypes.GroupSync:
|
||||
if (isSyncEnabled.current) {
|
||||
if (canReadTeamPermissions) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, Field, FieldSet, Input, Stack } from '@grafana/ui';
|
||||
import { Button, Divider, Field, FieldSet, Input, Stack } from '@grafana/ui';
|
||||
import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker';
|
||||
import { useRoleOptions } from 'app/core/components/RolePicker/hooks';
|
||||
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
@@ -97,6 +97,7 @@ const TeamSettings = ({ team }: Props) => {
|
||||
<Trans i18nKey="teams.team-settings.save">Save team details</Trans>
|
||||
</Button>
|
||||
</form>
|
||||
<Divider />
|
||||
<SharedPreferences resourceUri={`teams/${team.id}`} disabled={!canWriteTeamSettings} preferenceType="team" />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useCreateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import {
|
||||
useSearchTeamsQuery as useLegacySearchTeamsQuery,
|
||||
useCreateTeamMutation,
|
||||
@@ -127,14 +128,16 @@ export const useDeleteTeam = () => {
|
||||
export const useCreateTeam = () => {
|
||||
const [createTeam, response] = useCreateTeamMutation();
|
||||
const [setTeamRoles] = useSetTeamRolesMutation();
|
||||
const [createFolder] = useCreateFolder();
|
||||
|
||||
const trigger = async (team: CreateTeamCommand, pendingRoles?: Role[]) => {
|
||||
const trigger = async (team: CreateTeamCommand, pendingRoles?: Role[], createTeamFolder?: boolean) => {
|
||||
const mutationResult = await createTeam({
|
||||
createTeamCommand: team,
|
||||
});
|
||||
|
||||
const { data } = mutationResult;
|
||||
|
||||
// Add any pending roles to the team
|
||||
if (data && data.teamId && pendingRoles && pendingRoles.length) {
|
||||
await contextSrv.fetchUserPermissions();
|
||||
if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles()) {
|
||||
@@ -147,6 +150,14 @@ export const useCreateTeam = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data.teamId && createTeamFolder) {
|
||||
await createFolder({
|
||||
title: team.name,
|
||||
createAsTeamFolder: true,
|
||||
teamUid: data.uid,
|
||||
});
|
||||
}
|
||||
|
||||
return mutationResult;
|
||||
};
|
||||
|
||||
|
||||
@@ -59,6 +59,13 @@ export function buildNavModel(team: Team): NavModelItem {
|
||||
url: `org/teams/edit/${team.uid}/members`,
|
||||
});
|
||||
}
|
||||
navModel.children!.push({
|
||||
active: false,
|
||||
icon: 'folder',
|
||||
id: `team-resources-${team.uid}`,
|
||||
text: 'Resources',
|
||||
url: `org/teams/edit/${team.uid}/resources`,
|
||||
});
|
||||
|
||||
const teamGroupSync: NavModelItem = {
|
||||
active: false,
|
||||
|
||||
Reference in New Issue
Block a user