mirror of https://github.com/grafana/grafana
commit
c17140f263
@ -0,0 +1,125 @@ |
||||
import { css } from '@emotion/react'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
|
||||
export function getJsonFormatterStyles(theme: GrafanaTheme2) { |
||||
return css({ |
||||
'.json-formatter-row': { |
||||
fontFamily: 'monospace', |
||||
|
||||
'&, a, a:hover': { |
||||
color: theme.colors.text.primary, |
||||
textDecoration: 'none', |
||||
}, |
||||
|
||||
'.json-formatter-row': { |
||||
marginLeft: theme.spacing(2), |
||||
}, |
||||
|
||||
'.json-formatter-children': { |
||||
'&.json-formatter-empty': { |
||||
opacity: 0.5, |
||||
marginLeft: theme.spacing(2), |
||||
|
||||
'&::after': { |
||||
display: 'none', |
||||
}, |
||||
'&.json-formatter-object::after': { |
||||
content: "'No properties'", |
||||
}, |
||||
'&.json-formatter-array::after': { |
||||
content: "'[]'", |
||||
}, |
||||
}, |
||||
}, |
||||
|
||||
'.json-formatter-string': { |
||||
color: theme.isDark ? '#23d662' : 'green', |
||||
whiteSpace: 'pre-wrap', |
||||
wordWrap: 'break-word', |
||||
wordBreak: 'break-all', |
||||
}, |
||||
|
||||
'.json-formatter-number': { |
||||
color: theme.isDark ? theme.colors.primary.text : theme.colors.primary.main, |
||||
}, |
||||
'.json-formatter-boolean': { |
||||
color: theme.isDark ? theme.colors.primary.text : theme.colors.error.main, |
||||
}, |
||||
'.json-formatter-null': { |
||||
color: theme.isDark ? '#eec97d' : '#855a00', |
||||
}, |
||||
'.json-formatter-undefined': { |
||||
color: theme.isDark ? 'rgb(239, 143, 190)' : 'rgb(202, 11, 105)', |
||||
}, |
||||
'.json-formatter-function': { |
||||
color: theme.isDark ? '#fd48cb' : '#ff20ed', |
||||
}, |
||||
'.json-formatter-url': { |
||||
textDecoration: 'underline', |
||||
color: theme.isDark ? '#027bff' : theme.colors.primary.main, |
||||
cursor: 'pointer', |
||||
}, |
||||
|
||||
'.json-formatter-bracket': { |
||||
color: theme.isDark ? '#9494ff' : theme.colors.primary.main, |
||||
}, |
||||
'.json-formatter-key': { |
||||
color: theme.isDark ? '#23a0db' : '#00008b', |
||||
cursor: 'pointer', |
||||
paddingRight: theme.spacing(0.25), |
||||
marginRight: theme.spacing(0.5), |
||||
}, |
||||
|
||||
'.json-formatter-constructor-name': { |
||||
cursor: 'pointer', |
||||
}, |
||||
|
||||
'.json-formatter-array-comma': { |
||||
marginRight: theme.spacing(0.5), |
||||
}, |
||||
|
||||
'.json-formatter-toggler': { |
||||
lineHeight: '16px', |
||||
fontSize: theme.typography.size.xs, |
||||
verticalAlign: 'middle', |
||||
opacity: 0.6, |
||||
cursor: 'pointer', |
||||
paddingRight: theme.spacing(0.25), |
||||
|
||||
'&::after': { |
||||
display: 'inline-block', |
||||
transition: 'transform 100ms ease-in', |
||||
content: "'►'", |
||||
}, |
||||
}, |
||||
|
||||
// Inline preview on hover (optional)
|
||||
'> a > .json-formatter-preview-text': { |
||||
opacity: 0, |
||||
transition: 'opacity 0.15s ease-in', |
||||
fontStyle: 'italic', |
||||
}, |
||||
|
||||
'&:hover > a > .json-formatter-preview-text': { |
||||
opacity: 0.6, |
||||
}, |
||||
|
||||
// Open state
|
||||
'&.json-formatter-open': { |
||||
'> .json-formatter-toggler-link .json-formatter-toggler::after': { |
||||
transform: 'rotate(90deg)', |
||||
}, |
||||
'> .json-formatter-children::after': { |
||||
display: 'inline-block', |
||||
}, |
||||
'> a > .json-formatter-preview-text': { |
||||
display: 'none', |
||||
}, |
||||
'&.json-formatter-empty::after': { |
||||
display: 'block', |
||||
}, |
||||
}, |
||||
}, |
||||
}); |
||||
} |
@ -1,13 +1,214 @@ |
||||
package zanzana |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/openfga/openfga/assets" |
||||
"github.com/openfga/openfga/pkg/storage" |
||||
"github.com/openfga/openfga/pkg/storage/memory" |
||||
"github.com/openfga/openfga/pkg/storage/mysql" |
||||
"github.com/openfga/openfga/pkg/storage/postgres" |
||||
"github.com/openfga/openfga/pkg/storage/sqlcommon" |
||||
"xorm.io/xorm" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
// FIXME(kalleep): Add support for postgres, mysql and sqlite data stores.
|
||||
// Postgres and mysql is already implmented by openFGA so we just need to hook up migartions for them.
|
||||
// FIXME(kalleep): Add sqlite data store.
|
||||
// There is no support for sqlite atm but we are working on adding it: https://github.com/openfga/openfga/pull/1615
|
||||
func NewStore() storage.OpenFGADatastore { |
||||
return memory.New() |
||||
func NewStore(cfg *setting.Cfg, logger log.Logger) (storage.OpenFGADatastore, error) { |
||||
grafanaDBCfg, zanzanaDBCfg, err := parseConfig(cfg, logger) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to parse database config: %w", err) |
||||
} |
||||
|
||||
switch grafanaDBCfg.Type { |
||||
case migrator.SQLite: |
||||
return memory.New(), nil |
||||
case migrator.MySQL: |
||||
// For mysql we need to pass parseTime parameter in connection string
|
||||
connStr := grafanaDBCfg.ConnectionString + "&parseTime=true" |
||||
if err := runMigrations(cfg, migrator.MySQL, connStr, assets.MySQLMigrationDir); err != nil { |
||||
return nil, fmt.Errorf("failed to run migrations: %w", err) |
||||
} |
||||
return mysql.New(connStr, zanzanaDBCfg) |
||||
case migrator.Postgres: |
||||
if err := runMigrations(cfg, migrator.Postgres, grafanaDBCfg.ConnectionString, assets.PostgresMigrationDir); err != nil { |
||||
return nil, fmt.Errorf("failed to run migrations: %w", err) |
||||
} |
||||
return postgres.New(grafanaDBCfg.ConnectionString, zanzanaDBCfg) |
||||
} |
||||
|
||||
// Should never happen
|
||||
return nil, fmt.Errorf("unsupported database engine: %s", grafanaDBCfg.Type) |
||||
} |
||||
|
||||
func NewEmbeddedStore(cfg *setting.Cfg, db db.DB, logger log.Logger) (storage.OpenFGADatastore, error) { |
||||
grafanaDBCfg, zanzanaDBCfg, err := parseConfig(cfg, logger) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to parse database config: %w", err) |
||||
} |
||||
|
||||
m := migrator.NewMigrator(db.GetEngine(), cfg) |
||||
|
||||
switch grafanaDBCfg.Type { |
||||
case migrator.SQLite: |
||||
// FIXME(kalleep): At the moment sqlite is not a supported data store.
|
||||
// So we just return in memory store for now.
|
||||
return memory.New(), nil |
||||
case migrator.MySQL: |
||||
if err := runMigrationsWithMigrator(m, cfg, assets.MySQLMigrationDir); err != nil { |
||||
return nil, fmt.Errorf("failed to run migrations: %w", err) |
||||
} |
||||
|
||||
// For mysql we need to pass parseTime parameter in connection string
|
||||
return mysql.New(grafanaDBCfg.ConnectionString+"&parseTime=true", zanzanaDBCfg) |
||||
case migrator.Postgres: |
||||
if err := runMigrationsWithMigrator(m, cfg, assets.PostgresMigrationDir); err != nil { |
||||
return nil, fmt.Errorf("failed to run migrations: %w", err) |
||||
} |
||||
|
||||
return postgres.New(grafanaDBCfg.ConnectionString, zanzanaDBCfg) |
||||
} |
||||
|
||||
// Should never happen
|
||||
return nil, fmt.Errorf("unsupported database engine: %s", db.GetDialect().DriverName()) |
||||
} |
||||
|
||||
func parseConfig(cfg *setting.Cfg, logger log.Logger) (*sqlstore.DatabaseConfig, *sqlcommon.Config, error) { |
||||
sec := cfg.Raw.Section("database") |
||||
grafanaDBCfg, err := sqlstore.NewDatabaseConfig(cfg, nil) |
||||
if err != nil { |
||||
return nil, nil, nil |
||||
} |
||||
|
||||
zanzanaDBCfg := &sqlcommon.Config{ |
||||
Logger: newZanzanaLogger(logger), |
||||
// MaxTuplesPerWriteField: 0,
|
||||
// MaxTypesPerModelField: 0,
|
||||
MaxOpenConns: grafanaDBCfg.MaxOpenConn, |
||||
MaxIdleConns: grafanaDBCfg.MaxIdleConn, |
||||
ConnMaxLifetime: time.Duration(grafanaDBCfg.ConnMaxLifetime) * time.Second, |
||||
ExportMetrics: sec.Key("instrument_queries").MustBool(false), |
||||
} |
||||
|
||||
return grafanaDBCfg, zanzanaDBCfg, nil |
||||
} |
||||
|
||||
func runMigrations(cfg *setting.Cfg, typ, connStr, path string) error { |
||||
engine, err := xorm.NewEngine(typ, connStr) |
||||
if err != nil { |
||||
return fmt.Errorf("failed to parse database config: %w", err) |
||||
} |
||||
|
||||
m := migrator.NewMigrator(engine, cfg) |
||||
m.AddCreateMigration() |
||||
|
||||
return runMigrationsWithMigrator(m, cfg, path) |
||||
} |
||||
|
||||
func runMigrationsWithMigrator(m *migrator.Migrator, cfg *setting.Cfg, path string) error { |
||||
migrations, err := getMigrations(path) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, mig := range migrations { |
||||
m.AddMigration(mig.name, mig.migration) |
||||
} |
||||
|
||||
sec := cfg.Raw.Section("database") |
||||
return m.Start( |
||||
sec.Key("migration_locking").MustBool(true), |
||||
sec.Key("locking_attempt_timeout_sec").MustInt(), |
||||
) |
||||
} |
||||
|
||||
type migration struct { |
||||
name string |
||||
migration migrator.Migration |
||||
} |
||||
|
||||
func getMigrations(path string) ([]migration, error) { |
||||
entries, err := assets.EmbedMigrations.ReadDir(path) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to read migration dir: %w", err) |
||||
} |
||||
|
||||
// parseStatements extracts statements from a sql file so we can execute
|
||||
// them as separate migrations. OpenFGA uses Goose as their migration egine
|
||||
// and Goose uses a single sql file for both up and down migrations.
|
||||
// Grafana only supports up migration so we strip out the down migration
|
||||
// and parse each individual statement
|
||||
parseStatements := func(data []byte) ([]string, error) { |
||||
scripts := strings.Split(strings.TrimPrefix(string(data), "-- +goose Up"), "-- +goose Down") |
||||
if len(scripts) != 2 { |
||||
return nil, errors.New("malformed migration file") |
||||
} |
||||
|
||||
// We assume that up migrations are always before down migrations
|
||||
parts := strings.SplitAfter(scripts[0], ";") |
||||
stmts := make([]string, 0, len(parts)) |
||||
for _, p := range parts { |
||||
p = strings.TrimSpace(p) |
||||
if p != "" { |
||||
stmts = append(stmts, p) |
||||
} |
||||
} |
||||
|
||||
return stmts, nil |
||||
} |
||||
|
||||
formatName := func(name string) string { |
||||
// Each migration file start with XXX where X is a number.
|
||||
// We remove that part and prefix each migration with "zanzana".
|
||||
return strings.TrimSuffix("zanzana"+name[3:], ".sql") |
||||
} |
||||
|
||||
migrations := make([]migration, 0, len(entries)) |
||||
for _, e := range entries { |
||||
data, err := assets.EmbedMigrations.ReadFile(path + "/" + e.Name()) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to read migration file: %w", err) |
||||
} |
||||
|
||||
stmts, err := parseStatements(data) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to parse migration: %w", err) |
||||
} |
||||
|
||||
migrations = append(migrations, migration{ |
||||
name: formatName(e.Name()), |
||||
migration: &rawMigration{stmts: stmts}, |
||||
}) |
||||
} |
||||
|
||||
return migrations, nil |
||||
} |
||||
|
||||
var _ migrator.CodeMigration = (*rawMigration)(nil) |
||||
|
||||
type rawMigration struct { |
||||
stmts []string |
||||
migrator.MigrationBase |
||||
} |
||||
|
||||
func (m *rawMigration) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { |
||||
for _, stmt := range m.stmts { |
||||
if _, err := sess.Exec(stmt); err != nil { |
||||
return fmt.Errorf("failed to run migration: %w", err) |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (m *rawMigration) SQL(dialect migrator.Dialect) string { |
||||
return strings.Join(m.stmts, "\n") |
||||
} |
||||
|
@ -0,0 +1,21 @@ |
||||
[sample token] // NOT A REAL TOKEN |
||||
eyJUb2tlbiI6ImNvbXBsZXRlbHlfZmFrZV90b2tlbl9jZG9peTFhYzdwdXlwZCIsIkluc3RhbmNlIjp7IlN0YWNrSUQiOjEyMzQ1LCJTbHVnIjoic3R1Ymluc3RhbmNlIiwiUmVnaW9uU2x1ZyI6ImZha2UtcmVnaW9uIiwiQ2x1c3RlclNsdWciOiJmYWtlLWNsdXNlciJ9fQ== |
||||
|
||||
[create session} |
||||
curl -X POST -H "Content-Type: application/json" \ |
||||
http://admin:admin@localhost:3000/api/cloudmigration/migration \ |
||||
-d '{"AuthToken":"eyJUb2tlbiI6ImNvbXBsZXRlbHlfZmFrZV90b2tlbl9jZG9peTFhYzdwdXlwZCIsIkluc3RhbmNlIjp7IlN0YWNrSUQiOjEyMzQ1LCJTbHVnIjoic3R1Ymluc3RhbmNlIiwiUmVnaW9uU2x1ZyI6ImZha2UtcmVnaW9uIiwiQ2x1c3RlclNsdWciOiJmYWtlLWNsdXNlciJ9fQ=="}' |
||||
|
||||
[create snapshot] |
||||
curl -X POST -H "Content-Type: application/json" \ |
||||
http://admin:admin@localhost:3000/api/cloudmigration/migration/{sessionUid}/snapshot |
||||
|
||||
[get snapshot list] |
||||
curl -X GET http://admin:admin@localhost:3000/api/cloudmigration/migration/{sessionUid}/snapshots?limit=100&offset=0 |
||||
|
||||
[get snapshot] |
||||
curl -X GET http://admin:admin@localhost:3000/api/cloudmigration/migration/{sessionUid}/snapshot/{snapshotUid} |
||||
|
||||
[upload snapshot] |
||||
curl -X POST -H "Content-Type: application/json" \ |
||||
http://admin:admin@localhost:3000/api/cloudmigration/migration/{sessionUid}/snapshot/{snapshotUid}/upload |
@ -0,0 +1,234 @@ |
||||
package cloudmigrationimpl |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration" |
||||
"github.com/grafana/grafana/pkg/services/contexthandler" |
||||
"github.com/grafana/grafana/pkg/services/dashboards" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/services/folder" |
||||
"github.com/grafana/grafana/pkg/util/retryer" |
||||
) |
||||
|
||||
func (s *Service) getMigrationDataJSON(ctx context.Context) (*cloudmigration.MigrateDataRequest, error) { |
||||
// Data sources
|
||||
dataSources, err := s.getDataSources(ctx) |
||||
if err != nil { |
||||
s.log.Error("Failed to get datasources", "err", err) |
||||
return nil, err |
||||
} |
||||
|
||||
// Dashboards
|
||||
dashboards, err := s.getDashboards(ctx) |
||||
if err != nil { |
||||
s.log.Error("Failed to get dashboards", "err", err) |
||||
return nil, err |
||||
} |
||||
|
||||
// Folders
|
||||
folders, err := s.getFolders(ctx) |
||||
if err != nil { |
||||
s.log.Error("Failed to get folders", "err", err) |
||||
return nil, err |
||||
} |
||||
|
||||
migrationDataSlice := make( |
||||
[]cloudmigration.MigrateDataRequestItem, 0, |
||||
len(dataSources)+len(dashboards)+len(folders), |
||||
) |
||||
|
||||
for _, ds := range dataSources { |
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ |
||||
Type: cloudmigration.DatasourceDataType, |
||||
RefID: ds.UID, |
||||
Name: ds.Name, |
||||
Data: ds, |
||||
}) |
||||
} |
||||
|
||||
for _, dashboard := range dashboards { |
||||
dashboard.Data.Del("id") |
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ |
||||
Type: cloudmigration.DashboardDataType, |
||||
RefID: dashboard.UID, |
||||
Name: dashboard.Title, |
||||
Data: map[string]any{"dashboard": dashboard.Data}, |
||||
}) |
||||
} |
||||
|
||||
for _, f := range folders { |
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ |
||||
Type: cloudmigration.FolderDataType, |
||||
RefID: f.UID, |
||||
Name: f.Title, |
||||
Data: f, |
||||
}) |
||||
} |
||||
|
||||
migrationData := &cloudmigration.MigrateDataRequest{ |
||||
Items: migrationDataSlice, |
||||
} |
||||
|
||||
return migrationData, nil |
||||
} |
||||
|
||||
func (s *Service) getDataSources(ctx context.Context) ([]datasources.AddDataSourceCommand, error) { |
||||
dataSources, err := s.dsService.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) |
||||
if err != nil { |
||||
s.log.Error("Failed to get all datasources", "err", err) |
||||
return nil, err |
||||
} |
||||
|
||||
result := []datasources.AddDataSourceCommand{} |
||||
for _, dataSource := range dataSources { |
||||
// Decrypt secure json to send raw credentials
|
||||
decryptedData, err := s.secretsService.DecryptJsonData(ctx, dataSource.SecureJsonData) |
||||
if err != nil { |
||||
s.log.Error("Failed to decrypt secure json data", "err", err) |
||||
return nil, err |
||||
} |
||||
dataSourceCmd := datasources.AddDataSourceCommand{ |
||||
OrgID: dataSource.OrgID, |
||||
Name: dataSource.Name, |
||||
Type: dataSource.Type, |
||||
Access: dataSource.Access, |
||||
URL: dataSource.URL, |
||||
User: dataSource.User, |
||||
Database: dataSource.Database, |
||||
BasicAuth: dataSource.BasicAuth, |
||||
BasicAuthUser: dataSource.BasicAuthUser, |
||||
WithCredentials: dataSource.WithCredentials, |
||||
IsDefault: dataSource.IsDefault, |
||||
JsonData: dataSource.JsonData, |
||||
SecureJsonData: decryptedData, |
||||
ReadOnly: dataSource.ReadOnly, |
||||
UID: dataSource.UID, |
||||
} |
||||
result = append(result, dataSourceCmd) |
||||
} |
||||
return result, err |
||||
} |
||||
|
||||
func (s *Service) getFolders(ctx context.Context) ([]folder.Folder, error) { |
||||
reqCtx := contexthandler.FromContext(ctx) |
||||
folders, err := s.folderService.GetFolders(ctx, folder.GetFoldersQuery{ |
||||
SignedInUser: reqCtx.SignedInUser, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
result := make([]folder.Folder, len(folders)) |
||||
for i, folder := range folders { |
||||
result[i] = *folder |
||||
} |
||||
|
||||
return result, nil |
||||
} |
||||
|
||||
func (s *Service) getDashboards(ctx context.Context) ([]dashboards.Dashboard, error) { |
||||
dashs, err := s.dashboardService.GetAllDashboards(ctx) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
result := make([]dashboards.Dashboard, len(dashs)) |
||||
for i, dashboard := range dashs { |
||||
result[i] = *dashboard |
||||
} |
||||
|
||||
return result, nil |
||||
} |
||||
|
||||
// asynchronous process for writing the snapshot to the filesystem and updating the snapshot status
|
||||
func (s *Service) buildSnapshot(ctx context.Context, snapshotMeta cloudmigration.CloudMigrationSnapshot) { |
||||
// TODO -- make sure we can only build one snapshot at a time
|
||||
s.buildSnapshotMutex.Lock() |
||||
defer s.buildSnapshotMutex.Unlock() |
||||
s.buildSnapshotError = false |
||||
|
||||
// update snapshot status to creating, add some retries since this is a background task
|
||||
if err := retryer.Retry(func() (retryer.RetrySignal, error) { |
||||
err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ |
||||
UID: snapshotMeta.UID, |
||||
Status: cloudmigration.SnapshotStatusCreating, |
||||
}) |
||||
return retryer.FuncComplete, err |
||||
}, 10, time.Millisecond*100, time.Second*10); err != nil { |
||||
s.log.Error("failed to set snapshot status to 'creating'", "err", err) |
||||
s.buildSnapshotError = true |
||||
return |
||||
} |
||||
|
||||
// build snapshot
|
||||
// just sleep for now to simulate snapshot creation happening
|
||||
// need to do a couple of fancy things when we implement this:
|
||||
// - some sort of regular check-in so we know we haven't timed out
|
||||
// - a channel to listen for cancel events
|
||||
// - retries baked into the snapshot writing process?
|
||||
s.log.Debug("snapshot meta", "snapshot", snapshotMeta) |
||||
time.Sleep(3 * time.Second) |
||||
|
||||
// update snapshot status to pending upload with retry
|
||||
if err := retryer.Retry(func() (retryer.RetrySignal, error) { |
||||
err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ |
||||
UID: snapshotMeta.UID, |
||||
Status: cloudmigration.SnapshotStatusPendingUpload, |
||||
}) |
||||
return retryer.FuncComplete, err |
||||
}, 10, time.Millisecond*100, time.Second*10); err != nil { |
||||
s.log.Error("failed to set snapshot status to 'pending upload'", "err", err) |
||||
s.buildSnapshotError = true |
||||
} |
||||
} |
||||
|
||||
// asynchronous process for and updating the snapshot status
|
||||
func (s *Service) uploadSnapshot(ctx context.Context, snapshotMeta cloudmigration.CloudMigrationSnapshot) { |
||||
// TODO -- make sure we can only upload one snapshot at a time
|
||||
s.buildSnapshotMutex.Lock() |
||||
defer s.buildSnapshotMutex.Unlock() |
||||
s.buildSnapshotError = false |
||||
|
||||
// update snapshot status to uploading, add some retries since this is a background task
|
||||
if err := retryer.Retry(func() (retryer.RetrySignal, error) { |
||||
err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ |
||||
UID: snapshotMeta.UID, |
||||
Status: cloudmigration.SnapshotStatusUploading, |
||||
}) |
||||
return retryer.FuncComplete, err |
||||
}, 10, time.Millisecond*100, time.Second*10); err != nil { |
||||
s.log.Error("failed to set snapshot status to 'creating'", "err", err) |
||||
s.buildSnapshotError = true |
||||
return |
||||
} |
||||
|
||||
// upload snapshot
|
||||
// just sleep for now to simulate snapshot creation happening
|
||||
s.log.Debug("snapshot meta", "snapshot", snapshotMeta) |
||||
time.Sleep(3 * time.Second) |
||||
|
||||
// update snapshot status to pending processing with retry
|
||||
if err := retryer.Retry(func() (retryer.RetrySignal, error) { |
||||
err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ |
||||
UID: snapshotMeta.UID, |
||||
Status: cloudmigration.SnapshotStatusPendingProcessing, |
||||
}) |
||||
return retryer.FuncComplete, err |
||||
}, 10, time.Millisecond*100, time.Second*10); err != nil { |
||||
s.log.Error("failed to set snapshot status to 'pending upload'", "err", err) |
||||
s.buildSnapshotError = true |
||||
} |
||||
|
||||
// simulate the rest
|
||||
// processing
|
||||
time.Sleep(3 * time.Second) |
||||
if err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ |
||||
UID: snapshotMeta.UID, |
||||
Status: cloudmigration.SnapshotStatusProcessing, |
||||
}); err != nil { |
||||
s.log.Error("updating snapshot", "err", err) |
||||
} |
||||
// end here as the GetSnapshot handler will fill in the rest when called
|
||||
} |
|
@ -0,0 +1,69 @@ |
||||
import { totalFromStats } from './RuleStats'; |
||||
|
||||
describe('RuleStats', () => { |
||||
it('should count 0', () => { |
||||
expect( |
||||
totalFromStats({ |
||||
alerting: 0, |
||||
error: 0, |
||||
inactive: 0, |
||||
nodata: 0, |
||||
paused: 0, |
||||
pending: 0, |
||||
recording: 0, |
||||
}) |
||||
).toBe(0); |
||||
}); |
||||
|
||||
it('should count rules', () => { |
||||
expect( |
||||
totalFromStats({ |
||||
alerting: 2, |
||||
error: 0, |
||||
inactive: 0, |
||||
nodata: 0, |
||||
paused: 0, |
||||
pending: 2, |
||||
recording: 2, |
||||
}) |
||||
).toBe(6); |
||||
}); |
||||
|
||||
it('should not count rule health as a rule', () => { |
||||
expect( |
||||
totalFromStats({ |
||||
alerting: 0, |
||||
error: 1, |
||||
inactive: 1, |
||||
nodata: 0, |
||||
paused: 0, |
||||
pending: 0, |
||||
recording: 0, |
||||
}) |
||||
).toBe(1); |
||||
|
||||
expect( |
||||
totalFromStats({ |
||||
alerting: 0, |
||||
error: 0, |
||||
inactive: 0, |
||||
nodata: 1, |
||||
paused: 0, |
||||
pending: 0, |
||||
recording: 1, |
||||
}) |
||||
).toBe(1); |
||||
|
||||
expect( |
||||
totalFromStats({ |
||||
alerting: 0, |
||||
error: 0, |
||||
inactive: 1, |
||||
nodata: 0, |
||||
paused: 1, |
||||
pending: 0, |
||||
recording: 0, |
||||
}) |
||||
).toBe(1); |
||||
}); |
||||
}); |
@ -1,424 +0,0 @@ |
||||
/*! normalize.css commit fe56763 | MIT License | github.com/necolas/normalize.css */ |
||||
|
||||
// |
||||
// 1. Set default font family to sans-serif. |
||||
// 2. Prevent iOS and IE text size adjust after device orientation change, |
||||
// without disabling user zoom. |
||||
// |
||||
|
||||
html { |
||||
font-family: sans-serif; // 1 |
||||
-ms-text-size-adjust: 100%; // 2 |
||||
-webkit-text-size-adjust: 100%; // 2 |
||||
} |
||||
|
||||
// |
||||
// Remove default margin. |
||||
// |
||||
|
||||
body { |
||||
margin: 0; |
||||
} |
||||
|
||||
// HTML5 display definitions |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Correct `block` display not defined for any HTML5 element in IE 8/9. |
||||
// Correct `block` display not defined for `details` or `summary` in IE 10/11 |
||||
// and Firefox. |
||||
// Correct `block` display not defined for `main` in IE 11. |
||||
// |
||||
|
||||
article, |
||||
aside, |
||||
details, |
||||
figcaption, |
||||
figure, |
||||
footer, |
||||
header, |
||||
main, |
||||
menu, |
||||
nav, |
||||
section { |
||||
display: block; |
||||
} |
||||
|
||||
// |
||||
// 1. Correct `inline-block` display not defined in IE 8/9. |
||||
// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. |
||||
// |
||||
|
||||
audio, |
||||
canvas, |
||||
progress, |
||||
video { |
||||
display: inline-block; // 1 |
||||
vertical-align: baseline; // 2 |
||||
} |
||||
|
||||
// |
||||
// Prevent modern browsers from displaying `audio` without controls. |
||||
// Remove excess height in iOS 5 devices. |
||||
// |
||||
|
||||
audio:not([controls]) { |
||||
display: none; |
||||
height: 0; |
||||
} |
||||
|
||||
// |
||||
// Address `[hidden]` styling not present in IE 8/9/10. |
||||
// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. |
||||
// |
||||
|
||||
[hidden], |
||||
template { |
||||
display: none; |
||||
} |
||||
|
||||
// Links |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Remove the gray background color from active links in IE 10. |
||||
// |
||||
|
||||
a { |
||||
background-color: transparent; |
||||
} |
||||
|
||||
// |
||||
// Improve readability of focused elements when they are also in an |
||||
// active/hover state. |
||||
// |
||||
|
||||
a { |
||||
&:active { |
||||
outline: 0; |
||||
} |
||||
&:hover { |
||||
outline: 0; |
||||
} |
||||
} |
||||
|
||||
// Text-level semantics |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Address styling not present in IE 8/9/10/11, Safari, and Chrome. |
||||
// |
||||
|
||||
abbr[title] { |
||||
border-bottom: 1px dotted; |
||||
} |
||||
|
||||
// |
||||
// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. |
||||
// |
||||
|
||||
b, |
||||
strong { |
||||
font-weight: bold; |
||||
} |
||||
|
||||
// |
||||
// Address styling not present in Safari and Chrome. |
||||
// |
||||
|
||||
dfn { |
||||
font-style: italic; |
||||
} |
||||
|
||||
// |
||||
// Address variable `h1` font-size and margin within `section` and `article` |
||||
// contexts in Firefox 4+, Safari, and Chrome. |
||||
// |
||||
|
||||
h1 { |
||||
font-size: 2em; |
||||
margin: 0.67em 0; |
||||
} |
||||
|
||||
// |
||||
// Address styling not present in IE 8/9. |
||||
// |
||||
|
||||
mark { |
||||
background: #ff0; |
||||
color: #000; |
||||
} |
||||
|
||||
// |
||||
// Address inconsistent and variable font size in all browsers. |
||||
// |
||||
|
||||
small { |
||||
font-size: 80%; |
||||
} |
||||
|
||||
// |
||||
// Prevent `sub` and `sup` affecting `line-height` in all browsers. |
||||
// |
||||
|
||||
sub, |
||||
sup { |
||||
font-size: 75%; |
||||
line-height: 0; |
||||
position: relative; |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
sup { |
||||
top: -0.5em; |
||||
} |
||||
|
||||
sub { |
||||
bottom: -0.25em; |
||||
} |
||||
|
||||
// Embedded content |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Remove border when inside `a` element in IE 8/9/10. |
||||
// |
||||
|
||||
img { |
||||
border: 0; |
||||
} |
||||
|
||||
// |
||||
// Correct overflow not hidden in IE 9/10/11. |
||||
// |
||||
|
||||
svg:not(:root) { |
||||
overflow: hidden; |
||||
} |
||||
|
||||
// Grouping content |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Address margin not present in IE 8/9 and Safari. |
||||
// |
||||
|
||||
figure { |
||||
margin: 1em 40px; |
||||
} |
||||
|
||||
// |
||||
// Address differences between Firefox and other browsers. |
||||
// |
||||
|
||||
hr { |
||||
box-sizing: content-box; |
||||
height: 0; |
||||
} |
||||
|
||||
// |
||||
// Contain overflow in all browsers. |
||||
// |
||||
|
||||
pre { |
||||
overflow: auto; |
||||
} |
||||
|
||||
// |
||||
// Address odd `em`-unit font size rendering in all browsers. |
||||
// |
||||
|
||||
code, |
||||
kbd, |
||||
pre, |
||||
samp { |
||||
font-family: monospace, monospace; |
||||
font-size: 1em; |
||||
} |
||||
|
||||
// Forms |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Known limitation: by default, Chrome and Safari on OS X allow very limited |
||||
// styling of `select`, unless a `border` property is set. |
||||
// |
||||
|
||||
// |
||||
// 1. Correct color not being inherited. |
||||
// Known issue: affects color of disabled elements. |
||||
// 2. Correct font properties not being inherited. |
||||
// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. |
||||
// |
||||
|
||||
button, |
||||
input, |
||||
optgroup, |
||||
select, |
||||
textarea { |
||||
color: inherit; // 1 |
||||
font: inherit; // 2 |
||||
margin: 0; // 3 |
||||
} |
||||
|
||||
// |
||||
// Address `overflow` set to `hidden` in IE 8/9/10/11. |
||||
// |
||||
|
||||
button { |
||||
overflow: visible; |
||||
} |
||||
|
||||
// |
||||
// Address inconsistent `text-transform` inheritance for `button` and `select`. |
||||
// All other form control elements do not inherit `text-transform` values. |
||||
// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. |
||||
// Correct `select` style inheritance in Firefox. |
||||
// |
||||
|
||||
button, |
||||
select { |
||||
text-transform: none; |
||||
} |
||||
|
||||
// |
||||
// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` |
||||
// and `video` controls. |
||||
// 2. Correct inability to style clickable `input` types in iOS. |
||||
// 3. Improve usability and consistency of cursor style between image-type |
||||
// `input` and others. |
||||
// |
||||
|
||||
button, |
||||
html input[type='button'], |
||||
// 1 input[type='reset'], |
||||
input[type='submit'] { |
||||
-webkit-appearance: button; // 2 |
||||
cursor: pointer; // 3 |
||||
} |
||||
|
||||
// |
||||
// Re-set default cursor for disabled elements. |
||||
// |
||||
|
||||
button[disabled], |
||||
html input[disabled] { |
||||
cursor: default; |
||||
} |
||||
|
||||
// |
||||
// Remove inner padding and border in Firefox 4+. |
||||
// |
||||
|
||||
button::-moz-focus-inner, |
||||
input::-moz-focus-inner { |
||||
border: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
// |
||||
// Address Firefox 4+ setting `line-height` on `input` using `!important` in |
||||
// the UA stylesheet. |
||||
// |
||||
|
||||
input { |
||||
line-height: normal; |
||||
} |
||||
|
||||
// |
||||
// It's recommended that you don't attempt to style these elements. |
||||
// Firefox's implementation doesn't respect box-sizing, padding, or width. |
||||
// |
||||
// 1. Address box sizing set to `content-box` in IE 8/9/10. |
||||
// 2. Remove excess padding in IE 8/9/10. |
||||
// |
||||
|
||||
input[type='checkbox'], |
||||
input[type='radio'] { |
||||
box-sizing: border-box; // 1 |
||||
padding: 0; // 2 |
||||
} |
||||
|
||||
// |
||||
// Fix the cursor style for Chrome's increment/decrement buttons. For certain |
||||
// `font-size` values of the `input`, it causes the cursor style of the |
||||
// decrement button to change from `default` to `text`. |
||||
// |
||||
|
||||
input[type='number']::-webkit-inner-spin-button, |
||||
input[type='number']::-webkit-outer-spin-button { |
||||
height: auto; |
||||
} |
||||
|
||||
// |
||||
// Address `appearance` set to `searchfield` in Safari and Chrome. |
||||
// |
||||
|
||||
input[type='search'] { |
||||
-webkit-appearance: textfield; |
||||
} |
||||
|
||||
// |
||||
// Remove inner padding and search cancel button in Safari and Chrome on OS X. |
||||
// Safari (but not Chrome) clips the cancel button when the search input has |
||||
// padding (and `textfield` appearance). |
||||
// |
||||
|
||||
input[type='search']::-webkit-search-cancel-button, |
||||
input[type='search']::-webkit-search-decoration { |
||||
-webkit-appearance: none; |
||||
} |
||||
|
||||
// |
||||
// Define consistent border, margin, and padding. |
||||
// |
||||
|
||||
fieldset { |
||||
border: 1px solid #c0c0c0; |
||||
margin: 0 2px; |
||||
padding: 0.35em 0.625em 0.75em; |
||||
} |
||||
|
||||
// |
||||
// 1. Correct `color` not being inherited in IE 8/9/10/11. |
||||
// 2. Remove padding so people aren't caught out if they zero out fieldsets. |
||||
// |
||||
|
||||
legend { |
||||
border: 0; // 1 |
||||
padding: 0; // 2 |
||||
} |
||||
|
||||
// |
||||
// Remove default vertical scrollbar in IE 8/9/10/11. |
||||
// |
||||
|
||||
textarea { |
||||
overflow: auto; |
||||
} |
||||
|
||||
// |
||||
// Don't inherit the `font-weight` (applied by a rule above). |
||||
// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. |
||||
// |
||||
|
||||
optgroup { |
||||
font-weight: bold; |
||||
} |
||||
|
||||
// Tables |
||||
// ========================================================================== |
||||
|
||||
// |
||||
// Remove most spacing between table cells. |
||||
// |
||||
|
||||
table { |
||||
border-collapse: collapse; |
||||
border-spacing: 0; |
||||
} |
||||
|
||||
td, |
||||
th { |
||||
padding: 0; |
||||
} |
@ -1,122 +0,0 @@ |
||||
.json-formatter-row { |
||||
font-family: monospace; |
||||
|
||||
&, |
||||
a, |
||||
a:hover { |
||||
color: $json-explorer-default-color; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
.json-formatter-row { |
||||
margin-left: $space-md; |
||||
} |
||||
|
||||
.json-formatter-children { |
||||
&.json-formatter-empty { |
||||
opacity: 0.5; |
||||
margin-left: $space-md; |
||||
|
||||
&::after { |
||||
display: none; |
||||
} |
||||
&.json-formatter-object::after { |
||||
content: 'No properties'; |
||||
} |
||||
&.json-formatter-array::after { |
||||
content: '[]'; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.json-formatter-string { |
||||
color: $json-explorer-string-color; |
||||
white-space: pre-wrap; |
||||
word-wrap: break-word; |
||||
word-break: break-all; |
||||
} |
||||
|
||||
.json-formatter-number { |
||||
color: $json-explorer-number-color; |
||||
} |
||||
.json-formatter-boolean { |
||||
color: $json-explorer-boolean-color; |
||||
} |
||||
.json-formatter-null { |
||||
color: $json-explorer-null-color; |
||||
} |
||||
.json-formatter-undefined { |
||||
color: $json-explorer-undefined-color; |
||||
} |
||||
.json-formatter-function { |
||||
color: $json-explorer-function-color; |
||||
} |
||||
.json-formatter-date { |
||||
background-color: fade($json-explorer-default-color, 5%); |
||||
} |
||||
.json-formatter-url { |
||||
text-decoration: underline; |
||||
color: $json-explorer-url-color; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.json-formatter-bracket { |
||||
color: $json-explorer-bracket-color; |
||||
} |
||||
.json-formatter-key { |
||||
color: $json-explorer-key-color; |
||||
cursor: pointer; |
||||
padding-right: $space-xxs; |
||||
margin-right: 4px; |
||||
} |
||||
|
||||
.json-formatter-constructor-name { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.json-formatter-array-comma { |
||||
margin-right: 4px; |
||||
} |
||||
|
||||
.json-formatter-toggler { |
||||
line-height: 16px; |
||||
font-size: $font-size-xs; |
||||
vertical-align: middle; |
||||
opacity: $json-explorer-toggler-opacity; |
||||
cursor: pointer; |
||||
padding-right: $space-xxs; |
||||
|
||||
&::after { |
||||
display: inline-block; |
||||
transition: transform $json-explorer-rotate-time ease-in; |
||||
content: '►'; |
||||
} |
||||
} |
||||
|
||||
// Inline preview on hover (optional) |
||||
> a > .json-formatter-preview-text { |
||||
opacity: 0; |
||||
transition: opacity 0.15s ease-in; |
||||
font-style: italic; |
||||
} |
||||
|
||||
&:hover > a > .json-formatter-preview-text { |
||||
opacity: 0.6; |
||||
} |
||||
|
||||
// Open state |
||||
&.json-formatter-open { |
||||
> .json-formatter-toggler-link .json-formatter-toggler::after { |
||||
transform: rotate(90deg); |
||||
} |
||||
> .json-formatter-children::after { |
||||
display: inline-block; |
||||
} |
||||
> a > .json-formatter-preview-text { |
||||
display: none; |
||||
} |
||||
&.json-formatter-empty::after { |
||||
display: block; |
||||
} |
||||
} |
||||
} |
@ -1,155 +0,0 @@ |
||||
// TODO: this is used in Loki & Prometheus, move it |
||||
.explore-input-margin { |
||||
margin-right: 4px; |
||||
} |
||||
|
||||
.graph-legend { |
||||
flex-wrap: wrap; |
||||
} |
||||
|
||||
// TODO: move to Loki and Prometheus |
||||
.query-row-break { |
||||
flex-basis: 100%; |
||||
} |
||||
|
||||
// TODO: Prometheus-specifics, to be extracted to datasource soon |
||||
.explore { |
||||
.prom-query-field-info { |
||||
margin: 0.25em 0.5em 0.5em; |
||||
display: flex; |
||||
|
||||
details { |
||||
margin-left: 1em; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ReactTable basic overrides (does not include pivot/groups/filters) |
||||
// When integrating ReactTable as new panel plugin, move to _panel_table.scss |
||||
|
||||
.ReactTable { |
||||
border: none; |
||||
} |
||||
|
||||
.ReactTable .rt-table { |
||||
// Allow some space for the no-data text |
||||
min-height: 90px; |
||||
} |
||||
|
||||
.ReactTable .rt-thead.-header { |
||||
box-shadow: none; |
||||
background: $list-item-bg; |
||||
border-top: 2px solid $body-bg; |
||||
border-bottom: 2px solid $body-bg; |
||||
height: 2em; |
||||
} |
||||
|
||||
.ReactTable .rt-thead.-header .rt-th { |
||||
text-align: left; |
||||
color: $blue; |
||||
font-weight: $font-weight-semi-bold; |
||||
} |
||||
|
||||
.ReactTable .rt-thead .rt-td, |
||||
.ReactTable .rt-thead .rt-th { |
||||
padding: 0.45em 0 0.45em 1.1em; |
||||
border-right: none; |
||||
box-shadow: none; |
||||
} |
||||
|
||||
.ReactTable .rt-tbody .rt-td { |
||||
padding: 0.45em 0 0.45em 1.1em; |
||||
border-bottom: 2px solid $body-bg; |
||||
border-right: 2px solid $body-bg; |
||||
} |
||||
|
||||
.ReactTable .rt-tbody .rt-td:last-child { |
||||
border-right: none; |
||||
} |
||||
|
||||
.ReactTable .-pagination { |
||||
border-top: none; |
||||
box-shadow: none; |
||||
margin-top: $space-sm; |
||||
} |
||||
|
||||
.ReactTable .-pagination .-btn { |
||||
color: $blue; |
||||
background: $list-item-bg; |
||||
} |
||||
|
||||
.ReactTable .-pagination input, |
||||
.ReactTable .-pagination select { |
||||
color: $input-color; |
||||
background-color: $input-bg; |
||||
} |
||||
|
||||
.ReactTable .-loading { |
||||
background: $input-bg; |
||||
} |
||||
|
||||
.ReactTable .-loading.-active { |
||||
opacity: 0.8; |
||||
} |
||||
|
||||
.ReactTable .-loading > div { |
||||
color: $input-color; |
||||
} |
||||
|
||||
.ReactTable .rt-tr .rt-td:last-child { |
||||
text-align: right; |
||||
} |
||||
|
||||
.ReactTable .rt-noData { |
||||
top: 60px; |
||||
z-index: inherit; |
||||
} |
||||
|
||||
// React-component cascade fix: show "loading" when loading children |
||||
.rc-cascader-menu-item-loading:after { |
||||
position: absolute; |
||||
right: 12px; |
||||
content: 'loading'; |
||||
color: #767980; |
||||
font-style: italic; |
||||
} |
||||
|
||||
// React-component cascade fix: vertical alignment issue with Safari |
||||
.rc-cascader-menu { |
||||
vertical-align: top; |
||||
// To fix cascader button width issue in windows + firefox |
||||
scrollbar-width: thin; |
||||
} |
||||
|
||||
// TODO Experimental |
||||
|
||||
.cheat-sheet-item { |
||||
margin: $space-lg 0; |
||||
} |
||||
|
||||
.cheat-sheet-item__title { |
||||
font-size: $font-size-h3; |
||||
} |
||||
|
||||
.cheat-sheet-item__example { |
||||
margin: $space-xs 0; |
||||
// element is interactive, clear button styles |
||||
text-align: left; |
||||
border: none; |
||||
background: transparent; |
||||
display: block; |
||||
} |
||||
|
||||
.query-type-toggle { |
||||
margin-left: 5px; |
||||
|
||||
.btn.active { |
||||
background-color: $input-bg; |
||||
background-image: none; |
||||
background-clip: padding-box; |
||||
border: $input-border; |
||||
border-radius: $input-border-radius; |
||||
@include box-shadow($input-box-shadow); |
||||
color: $input-color; |
||||
} |
||||
} |
Loading…
Reference in new issue