3
0

Support for dimension step translations on charts

This commit is contained in:
Denis Arh 2022-07-06 08:04:28 +02:00
parent 235a483c15
commit bb23c84cf4
6 changed files with 482 additions and 71 deletions

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}

266
compose/types/chart_test.go Normal file
View File

@ -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"])
})
})
}

View File

@ -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"}