From bb23c84cf400d8ea1242d8b4806dd0b11733b8a0 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Wed, 6 Jul 2022 08:04:28 +0200 Subject: [PATCH] Support for dimension step translations on charts --- compose/chart.cue | 4 + compose/service/chart.go | 20 ++- compose/service/locale.go | 49 ++++--- compose/types/chart.go | 213 +++++++++++++++++++++++------ compose/types/chart_test.go | 266 ++++++++++++++++++++++++++++++++++++ compose/types/locale.gen.go | 1 + 6 files changed, 482 insertions(+), 71 deletions(-) create mode 100644 compose/types/chart_test.go diff --git a/compose/chart.cue b/compose/chart.cue index 402950d48..2560f81b7 100644 --- a/compose/chart.cue +++ b/compose/chart.cue @@ -55,6 +55,10 @@ chart: schema.#Resource & { path: ["metrics", {part: "metricID", var: true}, "label"] customHandler: true } + reportsDimensionStepLabel: { + path: ["dimensions", {part: "dimensionID", var: true}, "meta", "steps", {part: "stepID", var: true}, "label"] + customHandler: true + } } } diff --git a/compose/service/chart.go b/compose/service/chart.go index a94119eb9..ef3194862 100644 --- a/compose/service/chart.go +++ b/compose/service/chart.go @@ -2,16 +2,15 @@ package service import ( "context" - "github.com/cortezaproject/corteza-server/pkg/locale" - "reflect" - "strconv" - "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/pkg/label" + "github.com/cortezaproject/corteza-server/pkg/locale" "github.com/cortezaproject/corteza-server/store" + "reflect" + "strconv" ) type ( @@ -171,14 +170,8 @@ func (svc chart) Create(ctx context.Context, new *types.Chart) (*types.Chart, er new.UpdatedAt = nil new.DeletedAt = nil - // Ensure chart report IDs - for i, report := range new.Config.Reports { - new.Config.Reports[i].ReportID = nextID() - // Ensure chart report metric IDs - for j := range report.Metrics { - new.Config.Reports[i].Metrics[j]["metricID"] = strconv.FormatUint(nextID(), 10) - } - } + // generate config element IDs + new.Config.GenerateIDs(nextID) if err = store.CreateComposeChart(ctx, s, new); err != nil { return err @@ -269,6 +262,9 @@ func (svc chart) updater(ctx context.Context, namespaceID, chartID uint64, actio return err } + // generate config element IDs if missing + c.Config.GenerateIDs(nextID) + if changes&chartChanged > 0 { if err = store.UpdateComposeChart(ctx, s, c); err != nil { return err diff --git a/compose/service/locale.go b/compose/service/locale.go index 3942cbfe7..40c5c74c3 100644 --- a/compose/service/locale.go +++ b/compose/service/locale.go @@ -274,8 +274,9 @@ func (svc resourceTranslationsManager) pageExtended(ctx context.Context, res *ty func (svc resourceTranslationsManager) chartExtended(_ context.Context, res *types.Chart) (out locale.ResourceTranslationSet, err error) { var ( - yAxisLabelK = types.LocaleKeyChartYAxisLabel - metricLabelK = types.LocaleKeyChartMetricsMetricIDLabel + yAxisLabelK = types.LocaleKeyChartYAxisLabel + metricLabelK = types.LocaleKeyChartMetricsMetricIDLabel + dimStepLabelK = types.LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel ) for _, report := range res.Config.Reports { @@ -288,24 +289,36 @@ func (svc resourceTranslationsManager) chartExtended(_ context.Context, res *typ }) } - for _, metric := range report.Metrics { - if _, ok := metric["metricID"]; ok { - mID, is := metric["metricID"].(string) - if !is { - continue - } - mpl := strings.NewReplacer("{{metricID}}", mID) + report.WalkMetrics(func(metricID string, _ map[string]interface{}) { + mpl := strings.NewReplacer( + "{{metricID}}", metricID, + ) - for _, tag := range svc.locale.Tags() { - out = append(out, &locale.ResourceTranslation{ - Resource: res.ResourceTranslation(), - Lang: tag.String(), - Key: mpl.Replace(metricLabelK.Path), - Msg: svc.locale.TResourceFor(tag, res.ResourceTranslation(), mpl.Replace(metricLabelK.Path)), - }) - } + for _, tag := range svc.locale.Tags() { + out = append(out, &locale.ResourceTranslation{ + Resource: res.ResourceTranslation(), + Lang: tag.String(), + Key: mpl.Replace(metricLabelK.Path), + Msg: svc.locale.TResourceFor(tag, res.ResourceTranslation(), mpl.Replace(metricLabelK.Path)), + }) } - } + }) + + report.WalkDimensionSteps(func(dimensionID string, stepID string, _ map[string]interface{}) { + mpl := strings.NewReplacer( + "{{dimensionID}}", dimensionID, + "{{stepID}}", stepID, + ) + + for _, tag := range svc.locale.Tags() { + out = append(out, &locale.ResourceTranslation{ + Resource: res.ResourceTranslation(), + Lang: tag.String(), + Key: mpl.Replace(dimStepLabelK.Path), + Msg: svc.locale.TResourceFor(tag, res.ResourceTranslation(), mpl.Replace(dimStepLabelK.Path)), + }) + } + }) } return diff --git a/compose/types/chart.go b/compose/types/chart.go index df9019743..1805ca84b 100644 --- a/compose/types/chart.go +++ b/compose/types/chart.go @@ -4,6 +4,8 @@ import ( "database/sql/driver" "encoding/json" "github.com/cortezaproject/corteza-server/pkg/locale" + "github.com/spf13/cast" + "strconv" "strings" "time" @@ -72,25 +74,45 @@ func (c Chart) decodeTranslations(tt locale.ResourceTranslationIndex) { var aux *locale.ResourceTranslation for i, report := range c.Config.Reports { + if report == nil { + continue + } + + // apply translated label for YAxis if aux = tt.FindByKey(LocaleKeyChartYAxisLabel.Path); aux != nil { + if c.Config.Reports[i].YAxis == nil { + c.Config.Reports[i].YAxis = make(map[string]interface{}) + } + c.Config.Reports[i].YAxis["label"] = aux.Msg } - for j, metric := range report.Metrics { - if metricID, ok := metric["metricID"]; ok { - mID, is := metricID.(string) - if !is { - continue - } - mpl := strings.NewReplacer( - "{{metricID}}", mID, - ) + // apply translated labels for metrics + report.WalkMetrics(func(metricID string, metric map[string]interface{}) { + mpl := strings.NewReplacer("{{metricID}}", metricID) - if aux = tt.FindByKey(mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path)); aux != nil { - c.Config.Reports[i].Metrics[j]["label"] = aux.Msg - } + aux = tt.FindByKey(mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path)) + if aux == nil { + return } - } + + metric["label"] = aux.Msg + }) + + // apply translated labels for each dimension/step + report.WalkDimensionSteps(func(dimensionID, stepID string, step map[string]interface{}) { + mpl := strings.NewReplacer( + "{{dimensionID}}", dimensionID, + "{{stepID}}", stepID, + ) + + aux = tt.FindByKey(mpl.Replace(LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel.Path)) + if aux == nil { + return + } + + step["label"] = aux.Msg + }) } } @@ -98,41 +120,41 @@ func (c Chart) encodeTranslations() (out locale.ResourceTranslationSet) { out = make(locale.ResourceTranslationSet, 0, 12) for _, report := range c.Config.Reports { - if mLabel, ok := report.YAxis["label"]; ok { - ml, is := mLabel.(string) - if !is { - continue - } + // collect labels from chart config: YAxis + if _, ok := report.YAxis["label"]; ok { out = append(out, &locale.ResourceTranslation{ Resource: c.ResourceTranslation(), Key: LocaleKeyChartYAxisLabel.Path, - Msg: ml, + Msg: cast.ToString(report.YAxis["label"]), }) } - for _, metric := range report.Metrics { - if metricID, ok := metric["metricID"]; ok { - mID, is := metricID.(string) - if !is { - continue - } - mpl := strings.NewReplacer( - "{{metricID}}", mID, - ) + // collect labels from chart config: metrics + report.WalkMetrics(func(metricID string, m map[string]interface{}) { + mpl := strings.NewReplacer( + "{{metricID}}", metricID, + ) - if mLabel, ok := metric["label"]; ok { - ml, is := mLabel.(string) - if !is { - continue - } - out = append(out, &locale.ResourceTranslation{ - Resource: c.ResourceTranslation(), - Key: mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path), - Msg: ml, - }) - } - } - } + out = append(out, &locale.ResourceTranslation{ + Resource: c.ResourceTranslation(), + Key: mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path), + Msg: cast.ToString(m["label"]), + }) + }) + + // collect labels from chart config: dimensions/steps + report.WalkDimensionSteps(func(dimID, stepID string, step map[string]interface{}) { + mpl := strings.NewReplacer( + "{{dimensionID}}", dimID, + "{{stepID}}", stepID, + ) + + out = append(out, &locale.ResourceTranslation{ + Resource: c.ResourceTranslation(), + Key: mpl.Replace(LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel.Path), + Msg: cast.ToString(step["label"]), + }) + }) } return @@ -167,3 +189,112 @@ func (cc *ChartConfig) Scan(value interface{}) error { func (cc ChartConfig) Value() (driver.Value, error) { return json.Marshal(cc) } + +func (r *ChartConfigReport) WalkMetrics(fn func(string, map[string]interface{})) { + for m := range r.Metrics { + metricID, ok := r.Metrics[m]["metricID"] + if !ok { + continue + } + + if len(r.Metrics[m]) == 0 { + // avoid problems with nil maps + r.Metrics[m] = make(map[string]interface{}) + } + + fn(metricID.(string), r.Metrics[m]) + } +} + +func (r *ChartConfigReport) WalkDimensionSteps(fn func(string, string, map[string]interface{})) { + for d := range r.Dimensions { + dimensionID, ok := r.Dimensions[d]["dimensionID"] + if !ok { + continue + } + + meta, is := r.Dimensions[d]["meta"].(map[string]interface{}) + if !is { + continue + } + + var steps []map[string]interface{} + + switch aux := meta["steps"].(type) { + case []interface{}: + for _, i := range aux { + if kv, is := i.(map[string]interface{}); is { + steps = append(steps, kv) + } + } + case []map[string]interface{}: + steps = aux + } + + for s := range steps { + stepID, has := steps[s]["stepID"] + if !has { + return + } + + fn(dimensionID.(string), stepID.(string), steps[s]) + } + } +} + +func (c *ChartConfig) GenerateIDs(nextID func() uint64) { + // Ensure chart report IDs + for r := range c.Reports { + c.Reports[r].ReportID = nextID() + + // Ensure chart report metric IDs + for m := range c.Reports[r].Metrics { + met := c.Reports[r].Metrics[m] + if _, has := met["metricID"]; has { + continue + } + + met["metricID"] = strconv.FormatUint(nextID(), 10) + } + + for d := range c.Reports[r].Dimensions { + dim := c.Reports[r].Dimensions[d] + + if _, has := dim["dimensionID"]; !has { + dim["dimensionID"] = strconv.FormatUint(nextID(), 10) + } + + meta, is := dim["meta"].(map[string]interface{}) + if !is { + // no meta, no steps + continue + } + + var steps []map[string]interface{} + + switch aux := meta["steps"].(type) { + case []interface{}: + for _, i := range aux { + if kv, is := i.(map[string]interface{}); is { + steps = append(steps, kv) + } + } + case []map[string]interface{}: + steps = aux + } + + for s := range steps { + _, has := steps[s]["stepID"] + if has { + continue + } + + steps[s]["stepID"] = strconv.FormatUint(nextID(), 10) + } + + meta["steps"] = steps + dim["meta"] = meta + } + } + +} diff --git a/compose/types/chart_test.go b/compose/types/chart_test.go new file mode 100644 index 000000000..d8dbf852d --- /dev/null +++ b/compose/types/chart_test.go @@ -0,0 +1,266 @@ +package types + +import ( + "encoding/json" + "github.com/cortezaproject/corteza-server/pkg/locale" + "github.com/stretchr/testify/require" + "testing" +) + +func TestChart_decodeTranslations(t *testing.T) { + cc := []struct { + name string + base *ChartConfigReport + ccr *ChartConfigReport + tt locale.ResourceTranslationIndex + }{ + {"empty", &ChartConfigReport{}, &ChartConfigReport{}, nil}, + { + "XAxis label", + &ChartConfigReport{ + YAxis: map[string]interface{}{"label": ""}, + }, + &ChartConfigReport{ + YAxis: map[string]interface{}{"label": "new label"}, + }, + locale.ResourceTranslationIndex{ + "yAxis.label": &locale.ResourceTranslation{Msg: "new label"}, + }, + }, + { + "Metric labels", + &ChartConfigReport{ + Metrics: []map[string]interface{}{ + {"metricID": "112233"}, + }, + + Dimensions: []map[string]interface{}{ + { + "dimensionID": "223344", + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"stepID": "2233441"}, + {"stepID": "2233442"}, + }, + }, + }, + { + "dimensionID": "443322", + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"stepID": "4433221"}, + {"stepID": "4433222"}, + }, + }, + }, + }, + }, + &ChartConfigReport{ + Metrics: []map[string]interface{}{ + {"metricID": "112233", "label": "metric label"}, + }, + Dimensions: []map[string]interface{}{ + { + "dimensionID": "223344", + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"stepID": "2233441", "label": "Step label 1.1"}, + {"stepID": "2233442", "label": "Step label 1.2"}, + }, + }, + }, + { + "dimensionID": "443322", + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"stepID": "4433221", "label": "Step label 2.1"}, + {"stepID": "4433222", "label": "Step label 2.2"}, + }, + }, + }, + }, + }, + locale.ResourceTranslationIndex{ + "metrics.112233.label": &locale.ResourceTranslation{Msg: "metric label"}, + "dimensions.223344.meta.steps.2233441.label": &locale.ResourceTranslation{Msg: "Step label 1.1"}, + "dimensions.223344.meta.steps.2233442.label": &locale.ResourceTranslation{Msg: "Step label 1.2"}, + "dimensions.443322.meta.steps.4433221.label": &locale.ResourceTranslation{Msg: "Step label 2.1"}, + "dimensions.443322.meta.steps.4433222.label": &locale.ResourceTranslation{Msg: "Step label 2.2"}, + }, + }, + } + for _, c := range cc { + t.Run(c.name, func(t *testing.T) { + var ( + req = require.New(t) + chart = &Chart{Config: ChartConfig{Reports: []*ChartConfigReport{c.base}}} + ) + + chart.decodeTranslations(c.tt) + req.Equal(c.ccr, chart.Config.Reports[0]) + }) + } +} + +func TestChart_encodeTranslations(t *testing.T) { + cc := []struct { + name string + payload string + tt locale.ResourceTranslationSet + }{ + {"empty", "{}", locale.ResourceTranslationSet{}}, + { + "filled", + `{"reports": [{ + "YAxis": { "label": "YAxis label" }, + "reportID": "291579520866123964", + "filter": "YEAR(created_at) = YEAR(NOW()) AND QUARTER(created_at) = QUARTER(NOW())", + "moduleID": "285374676287488188", + "metrics": [ + { + "label": "metric label", + "metricID": "112233" + }, + { + "metricID": "223344" + } + ], + "dimensions": [{ + "conditions": {}, + "field": "Status", + "dimensionID": "11223344", + "meta": { + "steps": [ + { "stepID": "1111", "label": "aa", "value": "23" }, + { "stepID": "2222", "label": "bb", "value": "25" } + ] + }, + "modifier": "(no grouping / buckets)" + }]}]}`, + locale.ResourceTranslationSet{ + {Resource: "compose:chart/0/0", Key: "yAxis.label", Msg: "YAxis label"}, + {Resource: "compose:chart/0/0", Key: "metrics.112233.label", Msg: "metric label"}, + {Resource: "compose:chart/0/0", Key: "metrics.223344.label", Msg: ""}, + {Resource: "compose:chart/0/0", Key: "dimensions.11223344.meta.steps.1111.label", Msg: "aa"}, + {Resource: "compose:chart/0/0", Key: "dimensions.11223344.meta.steps.2222.label", Msg: "bb"}, + }, + }, + } + for _, c := range cc { + t.Run(c.name, func(t *testing.T) { + var ( + req = require.New(t) + chart = &Chart{Config: ChartConfig{}} + ) + + req.NoError(json.Unmarshal([]byte(c.payload), &chart.Config)) + result := chart.encodeTranslations() + req.Equal(c.tt, result) + }) + } +} + +func Test_GenerateConfigIDs(t *testing.T) { + var ( + r = &ChartConfigReport{ + Metrics: []map[string]interface{}{ + {"label": "metric label"}, + {}, + }, + Dimensions: []map[string]interface{}{ + { + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"label": "Step label 1.1"}, + {}, + }, + }, + }, + { + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"label": "Step label 2.1"}, + {}, + }, + }, + }, + }, + } + + c = &ChartConfig{Reports: []*ChartConfigReport{r}} + + i = uint64(0) + + req = require.New(t) + ) + + c.GenerateIDs(func() uint64 { + i++ + return i + }) + + req.EqualValues(1, r.ReportID) + req.Equal("2", r.Metrics[0]["metricID"]) + req.Equal("3", r.Metrics[1]["metricID"]) + req.Equal("4", r.Dimensions[0]["dimensionID"]) + req.Equal("5", r.Dimensions[0]["meta"].(map[string]interface{})["steps"].([]map[string]interface{})[0]["stepID"]) + req.Equal("6", r.Dimensions[0]["meta"].(map[string]interface{})["steps"].([]map[string]interface{})[1]["stepID"]) + req.Equal("7", r.Dimensions[1]["dimensionID"]) + req.Equal("8", r.Dimensions[1]["meta"].(map[string]interface{})["steps"].([]map[string]interface{})[0]["stepID"]) + req.Equal("9", r.Dimensions[1]["meta"].(map[string]interface{})["steps"].([]map[string]interface{})[1]["stepID"]) + +} + +func Test_ChartConfigReportWalkers(t *testing.T) { + var ( + r = &ChartConfigReport{ + Metrics: []map[string]interface{}{ + {"metricID": "M1", "label": ""}, + }, + Dimensions: []map[string]interface{}{ + { + "dimensionID": "D1", + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"stepID": "S1", "label": "-"}, + {"stepID": "S2", "label": "-"}, + }, + }, + }, + { + "dimensionID": "D2", + "meta": map[string]interface{}{ + "steps": []map[string]interface{}{ + {"stepID": "S1", "label": "-"}, + {"stepID": "S2", "label": "-"}, + }, + }, + }, + }, + } + ) + + t.Run("metrics", func(t *testing.T) { + req := require.New(t) + + r.WalkMetrics(func(mID string, m map[string]interface{}) { + m["label"] = mID + }) + + r.WalkMetrics(func(id string, m map[string]interface{}) { + req.Equal(id, m["label"]) + }) + }) + + t.Run("dimension-steps", func(t *testing.T) { + req := require.New(t) + + r.WalkDimensionSteps(func(dID string, sID string, m map[string]interface{}) { + m["label"] = dID + sID + }) + + r.WalkDimensionSteps(func(dID string, sID string, m map[string]interface{}) { + req.Equal(dID+sID, m["label"]) + }) + }) +} diff --git a/compose/types/locale.gen.go b/compose/types/locale.gen.go index 4a4c8bd35..cdc52f37d 100644 --- a/compose/types/locale.gen.go +++ b/compose/types/locale.gen.go @@ -34,6 +34,7 @@ var ( // @todo can we remove LocaleKey struct for string constant? LocaleKeyChartYAxisLabel = LocaleKey{Path: "yAxis.label"} LocaleKeyChartMetricsMetricIDLabel = LocaleKey{Path: "metrics.{{metricID}}.label"} + LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel = LocaleKey{Path: "dimensions.{{dimensionID}}.meta.steps.{{stepID}}.label"} LocaleKeyModuleName = LocaleKey{Path: "name"} LocaleKeyModuleFieldLabel = LocaleKey{Path: "label"} LocaleKeyModuleFieldMetaDescriptionView = LocaleKey{Path: "meta.description.view"}