From a2cf2c65ab545ec68a4002c62d381146a66be54a Mon Sep 17 00:00:00 2001
From: Ewe Zi Yi <36802364+deadlycoconuts@users.noreply.github.com>
Date: Thu, 3 Nov 2022 14:00:41 +0800
Subject: [PATCH] Add field selector to list experiments API (#44)
* Add field selector to list experiments API
* Refactor experiment service by creating more helper functions
* Specify table fields when UI queries API for experiments list
* Fix refactoring changes
* Update experiment context to query more experiments at once
* Remove all experiment fields from required field
* Make list experiments return paginated responses when page values are not specified
* Simplify api call to list experiments to only be performed once
* Fix changes to experiment models struct
* Update treatment service with changes to experiment model struct
* Refactor pagination to only take place when fields are not specified
* Fix bug whereby query fields were not passed to experiment service
* Make api call to display number of experiments retrieve only 1 field
* Simplify API calls to list experiments endpoint
* Fix bug whereby query fields were not passed to experiment service
* Revert changes to experiments config group by modifying experiments controller behaviour
* Refactor configs and contexts
* Refactor fieldNamesSet by using imported set
---
api/experiments.yaml | 9 +
api/schema.yaml | 22 +--
clients/management/managementclient.go | 20 +++
common/api/schema/schema.go | 122 +++++++------
management-service/api/api.go | 128 ++++++++------
.../controller/experiment_controller.go | 20 ++-
.../controller/experiment_controller_test.go | 13 +-
management-service/models/experiment.go | 100 +++++++++--
management-service/models/experiment_test.go | 91 ++++++++--
management-service/pagination/pagination.go | 2 +-
.../services/experiment_service.go | 164 +++++++++++++-----
.../services/experiment_service_test.go | 54 ++++--
.../fetch_treatment_it_test.go | 21 +--
treatment-service/models/storage_test.go | 33 ++--
treatment-service/models/typeconverter.go | 137 ++++++++++-----
.../models/typeconverter_test.go | 71 +++++---
.../testhelper/mockmanagement/api.go | 15 ++
.../mockmanagement/controller/experiment.go | 24 +--
.../mockmanagement/server/server_test.go | 66 ++++---
.../mockmanagement/service/store.go | 30 +++-
ui/src/components/table/BasicTable.js | 2 +-
ui/src/config.js | 1 +
.../experiments/list/ListExperimentsView.js | 1 +
ui/src/providers/experiment/context.js | 32 +---
.../standard_ensembler/LinkedRoutesTable.js | 13 +-
25 files changed, 791 insertions(+), 400 deletions(-)
diff --git a/api/experiments.yaml b/api/experiments.yaml
index c6c8d725..4f5f73a3 100644
--- a/api/experiments.yaml
+++ b/api/experiments.yaml
@@ -331,6 +331,15 @@ paths:
in: query
schema:
type: boolean
+ - name: fields
+ description: |
+ A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned.
+ Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided.
+ in: query
+ schema:
+ type: array
+ items:
+ $ref: 'schema.yaml#/components/schemas/ExperimentField'
responses:
200:
$ref: '#/components/responses/ListExperimentsSuccess'
diff --git a/api/schema.yaml b/api/schema.yaml
index fba21bec..ae335745 100644
--- a/api/schema.yaml
+++ b/api/schema.yaml
@@ -145,25 +145,19 @@ components:
$ref: '#/components/schemas/SelectedTreatmentData'
metadata:
$ref: '#/components/schemas/SelectedTreatmentMetadata'
- Experiment:
- required:
- - project_id
- - description
- - end_time
+ ExperimentField:
+ type: string
+ enum:
- id
- - interval
- name
- - segment
- - start_time
- - status
+ - type
- status_friendly
- - treatments
- tier
- - type
- - created_at
+ - start_time
+ - end_time
- updated_at
- - updated_by
- - version
+ - treatments
+ Experiment:
type: object
properties:
description:
diff --git a/clients/management/managementclient.go b/clients/management/managementclient.go
index 9e03e0ec..63ed575a 100644
--- a/clients/management/managementclient.go
+++ b/clients/management/managementclient.go
@@ -326,6 +326,10 @@ type ListExperimentsParams struct {
// controls whether or not weak segmenter matches (experiments where the segmenter is optional) should be returned
IncludeWeakMatch *bool `json:"include_weak_match,omitempty"`
+
+ // A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned.
+ // Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided.
+ Fields *[]externalRef0.ExperimentField `json:"fields,omitempty"`
}
// ListExperimentHistoryParams defines parameters for ListExperimentHistory.
@@ -1456,6 +1460,22 @@ func NewListExperimentsRequest(server string, projectId int64, params *ListExper
}
+ if params.Fields != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "fields", runtime.ParamLocationQuery, *params.Fields); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
queryURL.RawQuery = queryValues.Encode()
req, err := http.NewRequest("GET", queryURL.String(), nil)
diff --git a/common/api/schema/schema.go b/common/api/schema/schema.go
index 65cf9434..40fe4a12 100644
--- a/common/api/schema/schema.go
+++ b/common/api/schema/schema.go
@@ -18,6 +18,27 @@ import (
"github.com/pkg/errors"
)
+// Defines values for ExperimentField.
+const (
+ ExperimentFieldEndTime ExperimentField = "end_time"
+
+ ExperimentFieldId ExperimentField = "id"
+
+ ExperimentFieldName ExperimentField = "name"
+
+ ExperimentFieldStartTime ExperimentField = "start_time"
+
+ ExperimentFieldStatusFriendly ExperimentField = "status_friendly"
+
+ ExperimentFieldTier ExperimentField = "tier"
+
+ ExperimentFieldTreatments ExperimentField = "treatments"
+
+ ExperimentFieldType ExperimentField = "type"
+
+ ExperimentFieldUpdatedAt ExperimentField = "updated_at"
+)
+
// Defines values for ExperimentStatus.
const (
ExperimentStatusActive ExperimentStatus = "active"
@@ -105,29 +126,32 @@ type Error struct {
// Experiment defines model for Experiment.
type Experiment struct {
- CreatedAt time.Time `json:"created_at"`
- Description *string `json:"description"`
- EndTime time.Time `json:"end_time"`
- Id int64 `json:"id"`
- Interval *int32 `json:"interval"`
- Name string `json:"name"`
- ProjectId int64 `json:"project_id"`
- Segment ExperimentSegment `json:"segment"`
- StartTime time.Time `json:"start_time"`
- Status ExperimentStatus `json:"status"`
+ CreatedAt *time.Time `json:"created_at,omitempty"`
+ Description *string `json:"description"`
+ EndTime *time.Time `json:"end_time,omitempty"`
+ Id *int64 `json:"id,omitempty"`
+ Interval *int32 `json:"interval"`
+ Name *string `json:"name,omitempty"`
+ ProjectId *int64 `json:"project_id,omitempty"`
+ Segment *ExperimentSegment `json:"segment,omitempty"`
+ StartTime *time.Time `json:"start_time,omitempty"`
+ Status *ExperimentStatus `json:"status,omitempty"`
// The user-friendly classification of experiment statuses. The categories are
// self-explanatory. Note that the current time plays a role in the definition
// of some of these statuses.
- StatusFriendly ExperimentStatusFriendly `json:"status_friendly"`
- Tier ExperimentTier `json:"tier"`
- Treatments []ExperimentTreatment `json:"treatments"`
- Type ExperimentType `json:"type"`
- UpdatedAt time.Time `json:"updated_at"`
- UpdatedBy string `json:"updated_by"`
- Version int64 `json:"version"`
+ StatusFriendly *ExperimentStatusFriendly `json:"status_friendly,omitempty"`
+ Tier *ExperimentTier `json:"tier,omitempty"`
+ Treatments *[]ExperimentTreatment `json:"treatments,omitempty"`
+ Type *ExperimentType `json:"type,omitempty"`
+ UpdatedAt *time.Time `json:"updated_at,omitempty"`
+ UpdatedBy *string `json:"updated_by,omitempty"`
+ Version *int64 `json:"version,omitempty"`
}
+// ExperimentField defines model for ExperimentField.
+type ExperimentField string
+
// ExperimentHistory defines model for ExperimentHistory.
type ExperimentHistory struct {
CreatedAt time.Time `json:"created_at"`
@@ -486,38 +510,38 @@ func (a SegmenterOptions) MarshalJSON() ([]byte, error) {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
- "N5f/BwAA//+48xNDnioAAA==",
+ "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",
+ "WSVuYWh22r85/T8AAP//DYlTE2EqAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/management-service/api/api.go b/management-service/api/api.go
index e36e9960..5290e17c 100644
--- a/management-service/api/api.go
+++ b/management-service/api/api.go
@@ -328,6 +328,10 @@ type ListExperimentsParams struct {
// controls whether or not weak segmenter matches (experiments where the segmenter is optional) should be returned
IncludeWeakMatch *bool `json:"include_weak_match,omitempty"`
+
+ // A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned.
+ // Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided.
+ Fields *[]externalRef0.ExperimentField `json:"fields,omitempty"`
}
// ListExperimentHistoryParams defines parameters for ListExperimentHistory.
@@ -775,6 +779,18 @@ func (siw *ServerInterfaceWrapper) ListExperiments(w http.ResponseWriter, r *htt
return
}
+ // ------------- Optional query parameter "fields" -------------
+ if paramValue := r.URL.Query().Get("fields"); paramValue != "" {
+ paramsSet["fields"] = true
+
+ }
+
+ err = runtime.BindQueryParameter("form", true, false, "fields", r.URL.Query(), ¶ms.Fields)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Invalid format for parameter fields: %s", err), http.StatusBadRequest)
+ return
+ }
+
allSegmenters := map[string]interface{}{}
for k, v := range r.URL.Query() {
if _, contains := paramsSet[k]; !contains {
@@ -2216,64 +2232,64 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
+ "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",
- "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",
+ "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",
}
// GetSwagger returns the content of the embedded swagger specification file
diff --git a/management-service/controller/experiment_controller.go b/management-service/controller/experiment_controller.go
index f82dc08f..53673210 100644
--- a/management-service/controller/experiment_controller.go
+++ b/management-service/controller/experiment_controller.go
@@ -87,8 +87,12 @@ func (e ExperimentController) ListExperiments(w http.ResponseWriter, r *http.Req
return
}
var expsResp []schema.Experiment
+ var fields []models.ExperimentField
+ if listExperimentParams.Fields != nil {
+ fields = *listExperimentParams.Fields
+ }
for _, exp := range exps {
- expsResp = append(expsResp, exp.ToApiSchema(segmenterTypes))
+ expsResp = append(expsResp, exp.ToApiSchema(segmenterTypes, fields...))
}
Ok(w, expsResp, ToPagingSchema(paging))
@@ -334,7 +338,7 @@ func (e ExperimentController) toListExperimentParams(params api.ListExperimentsP
}
}
- return &services.ListExperimentsParams{
+ finalParams := services.ListExperimentsParams{
PaginationOptions: pagination.PaginationOptions{
Page: params.Page,
PageSize: params.PageSize,
@@ -350,5 +354,15 @@ func (e ExperimentController) toListExperimentParams(params api.ListExperimentsP
StartTime: params.StartTime,
Segment: validSegmentParam,
IncludeWeakMatch: params.IncludeWeakMatch != nil && *params.IncludeWeakMatch,
- }, nil
+ }
+
+ if params.Fields != nil {
+ var fields []models.ExperimentField
+ for _, field := range *params.Fields {
+ fields = append(fields, models.ExperimentField(field))
+ }
+ finalParams.Fields = &fields
+ }
+
+ return &finalParams, nil
}
diff --git a/management-service/controller/experiment_controller_test.go b/management-service/controller/experiment_controller_test.go
index 3470089d..50e25ba4 100644
--- a/management-service/controller/experiment_controller_test.go
+++ b/management-service/controller/experiment_controller_test.go
@@ -152,13 +152,12 @@ func (s *ExperimentControllerTestSuite) SetupSuite() {
Segment: models.ExperimentSegment{"days_of_week": []string{"1"}},
}).Return([]*models.Experiment{testExperiment}, nil, nil)
expSvc.
- On("ListExperiments", int64(2),
- services.ListExperimentsParams{
- Status: emptyStatus,
- StatusFriendly: []services.ExperimentStatusFriendly{},
- Type: emptyType,
- Segment: models.ExperimentSegment{}}).
- Return([]*models.Experiment{testExperiment}, nil, nil)
+ On("ListExperiments", int64(2), services.ListExperimentsParams{
+ Status: emptyStatus,
+ StatusFriendly: []services.ExperimentStatusFriendly{},
+ Type: emptyType,
+ Segment: models.ExperimentSegment{},
+ }).Return([]*models.Experiment{testExperiment}, nil, nil)
expSvc.
On("CreateExperiment",
models.Settings{ProjectID: models.ID(2)},
diff --git a/management-service/models/experiment.go b/management-service/models/experiment.go
index 8399caf0..2ede1964 100644
--- a/management-service/models/experiment.go
+++ b/management-service/models/experiment.go
@@ -14,6 +14,7 @@ import (
type ExperimentStatus string
type ExperimentType string
type ExperimentTier string
+type ExperimentField string
const (
ExperimentStatusActive ExperimentStatus = "active"
@@ -35,6 +36,30 @@ const (
ExperimentTierOverride ExperimentTier = "override"
)
+// Defines values for ExperimentField.
+const (
+ ExperimentFieldEndTime ExperimentField = "end_time"
+
+ ExperimentFieldId ExperimentField = "id"
+
+ ExperimentFieldName ExperimentField = "name"
+
+ ExperimentFieldStartTime ExperimentField = "start_time"
+
+ ExperimentFieldStatusFriendly ExperimentField = "status_friendly"
+
+ // ExperimentFieldStatus is only used for querying the db because status_friendly does not exist as a column
+ ExperimentFieldStatus ExperimentField = "status"
+
+ ExperimentFieldTier ExperimentField = "tier"
+
+ ExperimentFieldTreatments ExperimentField = "treatments"
+
+ ExperimentFieldType ExperimentField = "type"
+
+ ExperimentFieldUpdatedAt ExperimentField = "updated_at"
+)
+
type Experiment struct {
Model
@@ -83,25 +108,68 @@ func (e *Experiment) AfterFind(tx *gorm.DB) error {
// ToApiSchema converts the experiment DB model to a format compatible with the
// OpenAPI specifications.
-func (e *Experiment) ToApiSchema(segmentersType map[string]schema.SegmenterType) schema.Experiment {
+func (e *Experiment) ToApiSchema(segmentersType map[string]schema.SegmenterType, fields ...ExperimentField) schema.Experiment {
+ experiment := schema.Experiment{}
+
+ // Only return requested fields
+ if fields != nil {
+ for _, field := range fields {
+ switch field {
+ case ExperimentFieldName:
+ experiment.Name = &e.Name
+ case ExperimentFieldId:
+ id := e.ID.ToApiSchema()
+ experiment.Id = &id
+ case ExperimentFieldType:
+ experimentType := schema.ExperimentType(e.Type)
+ experiment.Type = &experimentType
+ case ExperimentFieldStatusFriendly:
+ statusFriendly := getExperimentStatusFriendly(e.StartTime, e.EndTime, e.Status)
+ experiment.StatusFriendly = &statusFriendly
+ case ExperimentFieldTier:
+ tier := schema.ExperimentTier(e.Tier)
+ experiment.Tier = &tier
+ case ExperimentFieldStartTime:
+ experiment.StartTime = &e.StartTime
+ case ExperimentFieldEndTime:
+ experiment.EndTime = &e.EndTime
+ case ExperimentFieldUpdatedAt:
+ experiment.UpdatedAt = &e.UpdatedAt
+ case ExperimentFieldTreatments:
+ treatments := e.Treatments.ToApiSchema()
+ experiment.Treatments = &treatments
+ }
+ }
+ return experiment
+ }
+
+ id := e.ID.ToApiSchema()
+ projectId := e.ProjectID.ToApiSchema()
+ segment := e.Segment.ToApiSchema(segmentersType)
+ status := schema.ExperimentStatus(e.Status)
+ statusFriendly := getExperimentStatusFriendly(e.StartTime, e.EndTime, e.Status)
+ treatments := e.Treatments.ToApiSchema()
+ experimentType := schema.ExperimentType(e.Type)
+ tier := schema.ExperimentTier(e.Tier)
+
return schema.Experiment{
Description: e.Description,
- EndTime: e.EndTime,
- Id: e.ID.ToApiSchema(),
+ EndTime: &e.EndTime,
+ Id: &id,
Interval: e.Interval,
- Name: e.Name,
- ProjectId: e.ProjectID.ToApiSchema(),
- Segment: e.Segment.ToApiSchema(segmentersType),
- Status: schema.ExperimentStatus(e.Status),
- StatusFriendly: getExperimentStatusFriendly(e.StartTime, e.EndTime, e.Status),
- Treatments: e.Treatments.ToApiSchema(),
- Type: schema.ExperimentType(e.Type),
- Tier: schema.ExperimentTier(e.Tier),
- StartTime: e.StartTime,
- CreatedAt: e.CreatedAt,
- UpdatedAt: e.UpdatedAt,
- UpdatedBy: e.UpdatedBy,
- Version: e.Version,
+ Name: &e.Name,
+ ProjectId: &projectId,
+ Segment: &segment,
+ Status: &status,
+ StatusFriendly: &statusFriendly,
+ Treatments: &treatments,
+ Type: &experimentType,
+ Tier: &tier,
+ StartTime: &e.StartTime,
+ CreatedAt: &e.CreatedAt,
+ UpdatedAt: &e.UpdatedAt,
+ UpdatedBy: &e.UpdatedBy,
+ Version: &e.Version,
}
}
diff --git a/management-service/models/experiment_test.go b/management-service/models/experiment_test.go
index 95aac8c7..93c18fc2 100644
--- a/management-service/models/experiment_test.go
+++ b/management-service/models/experiment_test.go
@@ -54,22 +54,36 @@ func TestExperimentToApiSchema(t *testing.T) {
"string_segmenter": schema.SegmenterTypeString,
}
+ id := int64(5)
+ projectId := int64(1)
+ createdAt := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC)
+ updatedAt := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC)
+ updatedBy := "admin"
+ endTime := time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC)
+ startTime := time.Date(2022, 2, 2, 1, 1, 1, 1, time.UTC)
+ name := "test-exp"
+ status := schema.ExperimentStatusActive
+ statusFriendly := schema.ExperimentStatusFriendlyCompleted
+ experimentType := schema.ExperimentTypeSwitchback
+ tier := schema.ExperimentTierDefault
+ version := int64(2)
+
assert.Equal(t, schema.Experiment{
- Id: int64(5),
- ProjectId: int64(1),
- CreatedAt: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC),
- UpdatedAt: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC),
- UpdatedBy: "admin",
- EndTime: time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC),
- StartTime: time.Date(2022, 2, 2, 1, 1, 1, 1, time.UTC),
- Name: "test-exp",
+ Id: &id,
+ ProjectId: &projectId,
+ CreatedAt: &createdAt,
+ UpdatedAt: &updatedAt,
+ UpdatedBy: &updatedBy,
+ EndTime: &endTime,
+ StartTime: &startTime,
+ Name: &name,
Description: &testExperimentDescription,
Interval: &testExperimentInterval,
- Status: schema.ExperimentStatusActive,
- StatusFriendly: schema.ExperimentStatusFriendlyCompleted,
- Type: schema.ExperimentTypeSwitchback,
- Tier: schema.ExperimentTierDefault,
- Treatments: []schema.ExperimentTreatment{
+ Status: &status,
+ StatusFriendly: &statusFriendly,
+ Type: &experimentType,
+ Tier: &tier,
+ Treatments: &[]schema.ExperimentTreatment{
{
Configuration: map[string]interface{}{
"config-1": "value",
@@ -79,13 +93,60 @@ func TestExperimentToApiSchema(t *testing.T) {
Traffic: &testExperimentTraffic,
},
},
- Segment: schema.ExperimentSegment{
+ Segment: &schema.ExperimentSegment{
"string_segmenter": []string{"seg-1"},
},
- Version: 2,
+ Version: &version,
}, testExperiment.ToApiSchema(segmenterTypes))
}
+func TestExperimentToApiSchemaWithFields(t *testing.T) {
+ segmenterTypes := map[string]schema.SegmenterType{
+ "string_segmenter": schema.SegmenterTypeString,
+ }
+
+ fields := []ExperimentField{
+ ExperimentFieldId,
+ ExperimentFieldName,
+ ExperimentFieldType,
+ ExperimentFieldStatusFriendly,
+ ExperimentFieldTier,
+ ExperimentFieldStartTime,
+ ExperimentFieldEndTime,
+ ExperimentFieldUpdatedAt,
+ ExperimentFieldTreatments,
+ }
+ id := int64(5)
+ updatedAt := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC)
+ endTime := time.Date(2022, 1, 1, 1, 1, 1, 1, time.UTC)
+ startTime := time.Date(2022, 2, 2, 1, 1, 1, 1, time.UTC)
+ name := "test-exp"
+ statusFriendly := schema.ExperimentStatusFriendlyCompleted
+ experimentType := schema.ExperimentTypeSwitchback
+ tier := schema.ExperimentTierDefault
+
+ assert.Equal(t, schema.Experiment{
+ Id: &id,
+ UpdatedAt: &updatedAt,
+ EndTime: &endTime,
+ StartTime: &startTime,
+ Name: &name,
+ StatusFriendly: &statusFriendly,
+ Type: &experimentType,
+ Tier: &tier,
+ Treatments: &[]schema.ExperimentTreatment{
+ {
+ Configuration: map[string]interface{}{
+ "config-1": "value",
+ "config-2": 2,
+ },
+ Name: "control",
+ Traffic: &testExperimentTraffic,
+ },
+ },
+ }, testExperiment.ToApiSchema(segmenterTypes, fields...))
+}
+
func TestExperimentToApiSchemaStatusFriendly(t *testing.T) {
tests := map[string]struct {
startTime time.Time
diff --git a/management-service/pagination/pagination.go b/management-service/pagination/pagination.go
index e45b1fc6..16f52002 100644
--- a/management-service/pagination/pagination.go
+++ b/management-service/pagination/pagination.go
@@ -7,7 +7,7 @@ import (
)
var (
- MaxPageSize int32 = 10
+ MaxPageSize int32 = 50
DefaultPageSize int32 = 10
DefaultPage int32 = 1
)
diff --git a/management-service/services/experiment_service.go b/management-service/services/experiment_service.go
index 6c609cc6..db842a14 100644
--- a/management-service/services/experiment_service.go
+++ b/management-service/services/experiment_service.go
@@ -66,6 +66,7 @@ type ListExperimentsParams struct {
StartTime *time.Time `json:"start_time,omitempty"`
Segment models.ExperimentSegment `json:"segment,omitempty"`
IncludeWeakMatch bool `json:"include_weak_match"`
+ Fields *[]models.ExperimentField `json:"fields,omitempty"`
}
type ExperimentService interface {
@@ -110,8 +111,18 @@ func (svc *experimentService) ListExperiments(
projectId int64,
params ListExperimentsParams,
) ([]*models.Experiment, *pagination.Paging, error) {
+ var err error
var exps []*models.Experiment
- query := svc.query().
+
+ query := svc.query()
+
+ // Handle Field values
+ query, err = svc.filterFieldValues(query, params)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ query = query.
Where("project_id = ?", projectId).
Order("updated_at desc")
@@ -119,34 +130,17 @@ func (svc *experimentService) ListExperiments(
if params.Status != nil {
query = query.Where("status = ?", params.Status)
}
+ // Handle StatusFriendly values
if len(params.StatusFriendly) > 0 {
query = svc.filterExperimentStatusFriendly(query, params.StatusFriendly)
}
- if params.StartTime != nil && !params.StartTime.IsZero() && (params.EndTime == nil || params.EndTime.IsZero()) {
- return nil, nil, errors.Newf(errors.BadInput, "end_time parameter must be supplied as well")
- }
- if params.EndTime != nil && !params.EndTime.IsZero() && (params.StartTime == nil || params.StartTime.IsZero()) {
- return nil, nil, errors.Newf(errors.BadInput, "start_time parameter must be supplied as well")
- }
- if params.StartTime != nil && !params.StartTime.IsZero() && params.EndTime != nil && !params.EndTime.IsZero() {
- // Find experiments that are at least partially running in this window.
- if params.StartTime.Equal(*params.EndTime) {
- // To filter active experiments at a given timestamp (such as current timestamp),
- // it needs to be passed in for both the start and end time.
- query = query.Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.EndTime)
- } else {
- // One of the following should match:
- // * the start_time parameter should fall within the experiment's [start and end) times
- // * the end_time parameter should fall within the experiment's (start and end) times
- // * the experiment starts and ends within the [start_time and end_time) duration
- query = query.Where(
- svc.query().
- Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.StartTime).
- Or("tstzrange(start_time, end_time, '()') @> tstzrange(?, ?, '[]')", params.EndTime, params.EndTime).
- Or("tstzrange(?, ?, '[]') @> tstzrange(start_time, end_time, '[)')", params.StartTime, params.EndTime),
- )
- }
+
+ // Handle Start and EndTime values
+ query, err = svc.filterStartEndTimeValues(query, params)
+ if err != nil {
+ return nil, nil, err
}
+
if params.Tier != nil {
query = query.Where("tier = ?", params.Tier)
}
@@ -172,22 +166,24 @@ func (svc *experimentService) ListExperiments(
// Pagination
var pagingResponse *pagination.Paging
var count int64
- err := pagination.ValidatePaginationParams(params.Page, params.PageSize)
- if err != nil {
- return nil, nil, err
- }
- pageOpts := pagination.NewPaginationOptions(params.Page, params.PageSize)
- // Count total
- query.Model(&exps).Count(&count)
- // Add offset and limit
- query = query.Offset(int((*pageOpts.Page - 1) * *pageOpts.PageSize))
- query = query.Limit(int(*pageOpts.PageSize))
- // Format opts into paging response
- pagingResponse = pagination.ToPaging(pageOpts, int(count))
- if pagingResponse.Page > 1 && pagingResponse.Pages < pagingResponse.Page {
- // Invalid query - total pages is less than the requested page
- return nil, nil, errors.Newf(errors.BadInput,
- "Requested page number %d exceeds total pages: %d.", pagingResponse.Page, pagingResponse.Pages)
+ if params.Fields == nil || params.Page != nil || params.PageSize != nil {
+ err = pagination.ValidatePaginationParams(params.Page, params.PageSize)
+ if err != nil {
+ return nil, nil, err
+ }
+ pageOpts := pagination.NewPaginationOptions(params.Page, params.PageSize)
+ // Count total
+ query.Model(&exps).Count(&count)
+ // Add offset and limit
+ query = query.Offset(int((*pageOpts.Page - 1) * *pageOpts.PageSize))
+ query = query.Limit(int(*pageOpts.PageSize))
+ // Format opts into paging response
+ pagingResponse = pagination.ToPaging(pageOpts, int(count))
+ if pagingResponse.Page > 1 && pagingResponse.Pages < pagingResponse.Page {
+ // Invalid query - total pages is less than the requested page
+ return nil, nil, errors.Newf(errors.BadInput,
+ "Requested page number %d exceeds total pages: %d.", pagingResponse.Page, pagingResponse.Pages)
+ }
}
// Filter experiments
@@ -555,6 +551,42 @@ func (svc *experimentService) save(exp *models.Experiment) (*models.Experiment,
return svc.GetDBRecord(exp.ProjectID, exp.ID)
}
+func (svc *experimentService) filterFieldValues(query *gorm.DB, params ListExperimentsParams) (*gorm.DB, error) {
+ if params.Fields != nil && len(*params.Fields) != 0 {
+ err := validateListExperimentFieldNames(*params.Fields)
+ if err != nil {
+ return nil, err
+ }
+
+ // Stores a set of unique field names which are unique column names to be selected in the db query
+ fieldNamesSet := set.New()
+ for _, field := range *params.Fields {
+ fieldName := field
+ // Add ExperimentFieldStatus to the query because status_friendly does not exist in the db as a column
+ if field == models.ExperimentFieldStatusFriendly {
+ // Add ExperimentFieldStartTime and ExperimentFieldEndTime to the query as they are required for
+ // determining the statusFriendly field; these two fields may or may not be returned depending on
+ // what is actually specified in params.Fields
+ fieldNamesSet.Insert(string(models.ExperimentFieldStartTime))
+ fieldNamesSet.Insert(string(models.ExperimentFieldEndTime))
+
+ fieldName = models.ExperimentFieldStatus
+ }
+ //fieldNamesSet[string(fieldName)] = struct{}{}
+ fieldNamesSet.Insert(string(fieldName))
+ }
+
+ // Retrieve a slice of unique strings from fieldNamesSet; query.Select only accepts []string
+ var fieldNames []string
+ fieldNamesSet.Do(func(fieldName interface{}) {
+ fieldNames = append(fieldNames, fmt.Sprint(fieldName))
+ })
+
+ query = query.Select(fieldNames)
+ }
+ return query, nil
+}
+
func (svc *experimentService) filterExperimentStatusFriendly(query *gorm.DB, statusesFriendly []ExperimentStatusFriendly) *gorm.DB {
orPredicates := svc.query().Where("false") // start with false and build OR query dynamically
for _, statusFriendly := range statusesFriendly {
@@ -578,6 +610,35 @@ func (svc *experimentService) filterExperimentStatusFriendly(query *gorm.DB, sta
return query.Where(orPredicates)
}
+func (svc *experimentService) filterStartEndTimeValues(query *gorm.DB, params ListExperimentsParams) (*gorm.DB, error) {
+ if params.StartTime != nil && !params.StartTime.IsZero() && (params.EndTime == nil || params.EndTime.IsZero()) {
+ return nil, errors.Newf(errors.BadInput, "end_time parameter must be supplied as well")
+ }
+ if params.EndTime != nil && !params.EndTime.IsZero() && (params.StartTime == nil || params.StartTime.IsZero()) {
+ return nil, errors.Newf(errors.BadInput, "start_time parameter must be supplied as well")
+ }
+ if params.StartTime != nil && !params.StartTime.IsZero() && params.EndTime != nil && !params.EndTime.IsZero() {
+ // Find experiments that are at least partially running in this window.
+ if params.StartTime.Equal(*params.EndTime) {
+ // To filter active experiments at a given timestamp (such as current timestamp),
+ // it needs to be passed in for both the start and end time.
+ query = query.Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.EndTime)
+ } else {
+ // One of the following should match:
+ // * the start_time parameter should fall within the experiment's [start and end) times
+ // * the end_time parameter should fall within the experiment's (start and end) times
+ // * the experiment starts and ends within the [start_time and end_time) duration
+ query = query.Where(
+ svc.query().
+ Where("tstzrange(start_time, end_time, '[)') @> tstzrange(?, ?, '[]')", params.StartTime, params.StartTime).
+ Or("tstzrange(start_time, end_time, '()') @> tstzrange(?, ?, '[]')", params.EndTime, params.EndTime).
+ Or("tstzrange(?, ?, '[]') @> tstzrange(start_time, end_time, '[)')", params.StartTime, params.EndTime),
+ )
+ }
+ }
+ return query, nil
+}
+
func (svc *experimentService) filterSegmenterValues(query *gorm.DB, segment models.ExperimentSegment, includeWeakMatch bool) *gorm.DB {
// No need to format the segmenter values according to their types since we're storing all values in string
for name, values := range segment {
@@ -586,6 +647,27 @@ func (svc *experimentService) filterSegmenterValues(query *gorm.DB, segment mode
return query
}
+func validateListExperimentFieldNames(fields []models.ExperimentField) error {
+ allowedFieldList := []interface{}{
+ models.ExperimentFieldId,
+ models.ExperimentFieldName,
+ models.ExperimentFieldStartTime,
+ models.ExperimentFieldEndTime,
+ models.ExperimentFieldTier,
+ models.ExperimentFieldType,
+ models.ExperimentFieldStatusFriendly,
+ models.ExperimentFieldUpdatedAt,
+ models.ExperimentFieldTreatments,
+ }
+ allowedFields := set.New(allowedFieldList...)
+ for _, field := range fields {
+ if !allowedFields.Has(field) {
+ return fmt.Errorf("field %s is not supported, fields should only be name and/or id", field)
+ }
+ }
+ return nil
+}
+
func (svc *experimentService) validateExperimentOrthogonality(
projectId int64,
experimentId *int64,
diff --git a/management-service/services/experiment_service_test.go b/management-service/services/experiment_service_test.go
index 848c1e50..db126b7d 100644
--- a/management-service/services/experiment_service_test.go
+++ b/management-service/services/experiment_service_test.go
@@ -163,28 +163,30 @@ func testListExperiments(s *ExperimentServiceTestSuite) {
stringSegmenter := []string{"seg-1"}
boolSegmenter := []string{"true"}
+ var nilPagingResponse *pagination.Paging
+
// All experiments under a settings
- expResponsesList, pagingResponse, err := svc.ListExperiments(1, services.ListExperimentsParams{})
+ actualResponsesList, pagingResponse, err := svc.ListExperiments(1, services.ListExperimentsParams{})
s.Suite.Require().NoError(err)
tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 3}, pagingResponse)
- tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1], s.Experiments[2]}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1], s.Experiments[2]}, actualResponsesList)
// No experiments filtered
- expResponsesList, pagingResponse, err = svc.ListExperiments(3, services.ListExperimentsParams{})
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(3, services.ListExperimentsParams{})
s.Suite.Require().NoError(err)
tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 0, Total: 0}, pagingResponse)
- tu.AssertEqualValues(t, []*models.Experiment{}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{}, actualResponsesList)
// Filter by a single parameter
- expResponsesList, pagingResponse, err = svc.ListExperiments(1,
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(1,
services.ListExperimentsParams{Status: &testStatus},
)
s.Suite.Require().NoError(err)
tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 2}, pagingResponse)
- tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, actualResponsesList)
// Filter by all parameters
- expResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(1, services.ListExperimentsParams{
Type: &testExpType,
Status: &testStatus,
StartTime: &testStartTime,
@@ -208,11 +210,11 @@ func testListExperiments(s *ExperimentServiceTestSuite) {
Pages: 1,
Total: 1,
}, pagingResponse)
- tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0]}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0]}, actualResponsesList)
// Use the same start and end times
testExactTimestamp := time.Date(2021, 2, 2, 3, 5, 7, 0, time.UTC)
- expResponsesList, pagingResponse, err = svc.ListExperiments(1,
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(1,
services.ListExperimentsParams{
StartTime: &testExactTimestamp,
EndTime: &testExactTimestamp,
@@ -220,10 +222,10 @@ func testListExperiments(s *ExperimentServiceTestSuite) {
)
s.Suite.Require().NoError(err)
tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 1}, pagingResponse)
- tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[2]}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[2]}, actualResponsesList)
// Partial match of segmenter on multiple experiments
- expResponsesList, pagingResponse, err = svc.ListExperiments(1,
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(1,
services.ListExperimentsParams{
Segment: models.ExperimentSegment{
"float_segmenter": float2Segmenter,
@@ -234,11 +236,11 @@ func testListExperiments(s *ExperimentServiceTestSuite) {
tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 2}, pagingResponse)
tu.AssertEqualValues(t,
[]*models.Experiment{s.Experiments[0], s.Experiments[1]},
- expResponsesList,
+ actualResponsesList,
)
// Weak match of segmenters
- expResponsesList, pagingResponse, err = svc.ListExperiments(1,
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(1,
services.ListExperimentsParams{
Segment: models.ExperimentSegment{
"float_segmenter": floatSegmenter,
@@ -250,21 +252,21 @@ func testListExperiments(s *ExperimentServiceTestSuite) {
tu.AssertEqualValues(t, &pagination.Paging{Page: 1, Pages: 1, Total: 2}, pagingResponse)
tu.AssertEqualValues(t,
[]*models.Experiment{s.Experiments[0], s.Experiments[2]},
- expResponsesList,
+ actualResponsesList,
)
// Match name or description
testDesc := "-1"
- expResponsesList, _, err = svc.ListExperiments(1,
+ actualResponsesList, _, err = svc.ListExperiments(1,
services.ListExperimentsParams{
Search: &testDesc,
},
)
s.Suite.Require().NoError(err)
- tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[2]}, actualResponsesList)
// Match friendly status + start time
- expResponsesList, _, err = svc.ListExperiments(1,
+ actualResponsesList, _, err = svc.ListExperiments(1,
services.ListExperimentsParams{
StatusFriendly: []services.ExperimentStatusFriendly{
services.ExperimentStatusFriendlyCompleted,
@@ -275,7 +277,23 @@ func testListExperiments(s *ExperimentServiceTestSuite) {
},
)
s.Suite.Require().NoError(err)
- tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1]}, expResponsesList)
+ tu.AssertEqualValues(t, []*models.Experiment{s.Experiments[0], s.Experiments[1]}, actualResponsesList)
+
+ // Specify selected fields in ListExperimentsParams
+ actualResponsesList, pagingResponse, err = svc.ListExperiments(1,
+ services.ListExperimentsParams{
+ Fields: &[]models.ExperimentField{
+ models.ExperimentFieldName,
+ },
+ },
+ )
+ s.Suite.Require().NoError(err)
+ tu.AssertEqualValues(t, nilPagingResponse, pagingResponse)
+ tu.AssertEqualValues(t, []*models.Experiment{
+ {Name: s.Experiments[0].Name},
+ {Name: s.Experiments[1].Name},
+ {Name: s.Experiments[2].Name},
+ }, actualResponsesList)
}
func testCreateUpdateExperiment(s *ExperimentServiceTestSuite) {
diff --git a/treatment-service/integration-test/fetch_treatment_it_test.go b/treatment-service/integration-test/fetch_treatment_it_test.go
index ab0ad840..3ba247fd 100644
--- a/treatment-service/integration-test/fetch_treatment_it_test.go
+++ b/treatment-service/integration-test/fetch_treatment_it_test.go
@@ -204,23 +204,24 @@ func generateExperiments() []schema.Experiment {
experimentType := val["type"].(schema.ExperimentType)
startTime := val["start-time"].(time.Time)
endTime := val["end-time"].(time.Time)
+ version := int64(1)
experiment := schema.Experiment{
- Id: id,
- ProjectId: projectId,
+ Id: &id,
+ ProjectId: &projectId,
Description: &description,
- EndTime: endTime,
+ EndTime: &endTime,
Interval: &interval,
- Name: name,
- Segment: schema.ExperimentSegment{
+ Name: &name,
+ Segment: &schema.ExperimentSegment{
"days_of_week": &daysOfWeek,
"hours_of_day": &hoursOfDay,
"s2_ids": &s2Ids,
},
- StartTime: startTime,
- Treatments: treatments,
- Type: experimentType,
- Version: int64(1),
+ StartTime: &startTime,
+ Treatments: &treatments,
+ Type: &experimentType,
+ Version: &version,
}
experiments = append(experiments, experiment)
}
@@ -811,5 +812,5 @@ func (suite *TreatmentServiceTestSuite) TestLocalStorage() {
resp, err := suite.managementServiceClient.GetExperimentWithResponse(suite.ctx, 1, 1)
suite.Require().NoError(err)
- suite.Require().Equal(resp.JSON200.Data.Name, storage.Experiments[1][0].Experiment.Name)
+ suite.Require().Equal(storage.Experiments[1][0].Experiment.Name, *resp.JSON200.Data.Name)
}
diff --git a/treatment-service/models/storage_test.go b/treatment-service/models/storage_test.go
index 3d4ad331..2a1cf728 100644
--- a/treatment-service/models/storage_test.go
+++ b/treatment-service/models/storage_test.go
@@ -5,7 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"math/rand"
"net/http"
"os"
@@ -51,18 +51,25 @@ func newTestXPExperiment(
{Configuration: make(map[string]interface{}), Name: "default", Traffic: &traffic},
}
+ id := int64(rand.Intn(100))
+ nameString := name.String()
+ status := schema.ExperimentStatusActive
+ experimentType := schema.ExperimentTypeAB
+ updatedAt := time.Time{}
+ updatedBy := ""
+
return schema.Experiment{
- ProjectId: projectId,
- Id: int64(rand.Intn(100)),
- StartTime: startTime,
- EndTime: endTime,
- Name: name.String(),
- Status: "active",
- Segment: segment,
- Treatments: treatments,
- Type: "A/B",
- UpdatedAt: time.Time{},
- UpdatedBy: "",
+ ProjectId: &projectId,
+ Id: &id,
+ StartTime: &startTime,
+ EndTime: &endTime,
+ Name: &nameString,
+ Status: &status,
+ Segment: &segment,
+ Treatments: &treatments,
+ Type: &experimentType,
+ UpdatedAt: &updatedAt,
+ UpdatedBy: &updatedBy,
Interval: &interval,
}
}
@@ -110,7 +117,7 @@ func (suite *LocalStorageLookupSuite) SetupTest() {
Return(&http.Response{
StatusCode: 200,
Header: map[string][]string{"Content-Type": {"json"}},
- Body: ioutil.NopCloser(bytes.NewBufferString(`{"data" : []}`)),
+ Body: io.NopCloser(bytes.NewBufferString(`{"data" : []}`)),
}, nil)
suite.storage = LocalStorage{
diff --git a/treatment-service/models/typeconverter.go b/treatment-service/models/typeconverter.go
index 1ce81c4f..c9b80860 100644
--- a/treatment-service/models/typeconverter.go
+++ b/treatment-service/models/typeconverter.go
@@ -2,6 +2,7 @@ package models
import (
"reflect"
+ "time"
"github.com/caraml-dev/xp/common/api/schema"
_pubsub "github.com/caraml-dev/xp/common/pubsub"
@@ -66,72 +67,112 @@ func OpenAPIExperimentSpecToProtobuf(
"default": _pubsub.Experiment_Default, "override": _pubsub.Experiment_Override,
}
+ var status _pubsub.Experiment_Status
+ if xpExperiment.Status != nil {
+ status = statusConverter[*xpExperiment.Status]
+ }
+
+ var experimentType _pubsub.Experiment_Type
+ if xpExperiment.Type != nil {
+ experimentType = typeConverter[*xpExperiment.Type]
+ }
+
+ var tier _pubsub.Experiment_Tier
+ if xpExperiment.Tier != nil {
+ tier = tierConverter[*xpExperiment.Tier]
+ }
+
segments := make(map[string]*_segmenters.ListSegmenterValue)
- for key, val := range xpExperiment.Segment {
- vals := val.([]interface{})
- switch segmentersType[key] {
- case "string":
- stringVals := []string{}
- for _, val := range vals {
- stringVals = append(stringVals, val.(string))
- }
- segments[key] = _utils.StringSliceToListSegmenterValue(&stringVals)
- case "integer":
- intVals := []int64{}
- for _, val := range vals {
- reflectedVal := reflect.ValueOf(val)
- switch reflectedVal.Kind() {
- case reflect.Float32, reflect.Float64:
- intVals = append(intVals, int64(reflectedVal.Float()))
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- intVals = append(intVals, reflectedVal.Int())
+
+ if xpExperiment.Segment != nil {
+ for key, val := range *xpExperiment.Segment {
+ vals := val.([]interface{})
+ switch segmentersType[key] {
+ case "string":
+ stringVals := []string{}
+ for _, val := range vals {
+ stringVals = append(stringVals, val.(string))
}
+ segments[key] = _utils.StringSliceToListSegmenterValue(&stringVals)
+ case "integer":
+ intVals := []int64{}
+ for _, val := range vals {
+ reflectedVal := reflect.ValueOf(val)
+ switch reflectedVal.Kind() {
+ case reflect.Float32, reflect.Float64:
+ intVals = append(intVals, int64(reflectedVal.Float()))
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ intVals = append(intVals, reflectedVal.Int())
+ }
+ }
+ segments[key] = _utils.Int64ListToListSegmenterValue(&intVals)
+ case "real":
+ floatVals := []float64{}
+ for _, val := range vals {
+ floatVals = append(floatVals, val.(float64))
+ }
+ segments[key] = _utils.FloatListToListSegmenterValue(&floatVals)
+ case "bool":
+ boolVals := []bool{}
+ for _, val := range vals {
+ boolVals = append(boolVals, val.(bool))
+ }
+ segments[key] = _utils.BoolSliceToListSegmenterValue(&boolVals)
+ default:
+ segments[key] = nil
}
- segments[key] = _utils.Int64ListToListSegmenterValue(&intVals)
- case "real":
- floatVals := []float64{}
- for _, val := range vals {
- floatVals = append(floatVals, val.(float64))
- }
- segments[key] = _utils.FloatListToListSegmenterValue(&floatVals)
- case "bool":
- boolVals := []bool{}
- for _, val := range vals {
- boolVals = append(boolVals, val.(bool))
- }
- segments[key] = _utils.BoolSliceToListSegmenterValue(&boolVals)
- default:
- segments[key] = nil
}
}
treatments := make([]*_pubsub.ExperimentTreatment, 0)
- for _, t := range xpExperiment.Treatments {
- treatment, err := openAPIExperimentTreatmentSpecToProtobuf(t)
- if err != nil {
- return nil, err
+ if xpExperiment.Treatments != nil {
+ for _, t := range *xpExperiment.Treatments {
+ treatment, err := openAPIExperimentTreatmentSpecToProtobuf(t)
+ if err != nil {
+ return nil, err
+ }
+ treatments = append(treatments, treatment)
}
- treatments = append(treatments, treatment)
}
interval := int32(0)
if xpExperiment.Interval != nil {
interval = *xpExperiment.Interval
}
+ var version int64
+ if xpExperiment.Version != nil {
+ version = *xpExperiment.Version
+ }
+
+ var startTime time.Time
+ if xpExperiment.StartTime != nil {
+ startTime = *xpExperiment.StartTime
+ }
+
+ var endTime time.Time
+ if xpExperiment.EndTime != nil {
+ endTime = *xpExperiment.EndTime
+ }
+
+ var updatedAt time.Time
+ if xpExperiment.UpdatedAt != nil {
+ updatedAt = *xpExperiment.UpdatedAt
+ }
+
return &_pubsub.Experiment{
- Id: xpExperiment.Id,
- ProjectId: xpExperiment.ProjectId,
- Status: statusConverter[xpExperiment.Status],
- Name: xpExperiment.Name,
- Type: typeConverter[xpExperiment.Type],
- Tier: tierConverter[xpExperiment.Tier],
+ Id: *xpExperiment.Id,
+ ProjectId: *xpExperiment.ProjectId,
+ Status: status,
+ Name: *xpExperiment.Name,
+ Type: experimentType,
+ Tier: tier,
Interval: interval,
Segments: segments,
Treatments: treatments,
- StartTime: ×tamppb.Timestamp{Seconds: xpExperiment.StartTime.Unix()},
- EndTime: ×tamppb.Timestamp{Seconds: xpExperiment.EndTime.Unix()},
- UpdatedAt: ×tamppb.Timestamp{Seconds: xpExperiment.UpdatedAt.Unix()},
- Version: xpExperiment.Version,
+ StartTime: ×tamppb.Timestamp{Seconds: startTime.Unix()},
+ EndTime: ×tamppb.Timestamp{Seconds: endTime.Unix()},
+ UpdatedAt: ×tamppb.Timestamp{Seconds: updatedAt.Unix()},
+ Version: version,
}, nil
}
diff --git a/treatment-service/models/typeconverter_test.go b/treatment-service/models/typeconverter_test.go
index 025378f4..2c6f0c81 100644
--- a/treatment-service/models/typeconverter_test.go
+++ b/treatment-service/models/typeconverter_test.go
@@ -70,6 +70,20 @@ func TestOpenAPIProjectSettingsSpecToProtobuf(t *testing.T) {
}
func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) {
+ projectId := int64(1)
+ id := int64(2)
+ name := "experiment-1"
+ statusActive := schema.ExperimentStatusActive
+ statusInactive := schema.ExperimentStatusInactive
+ tierDefault := schema.ExperimentTierDefault
+ tierOverride := schema.ExperimentTierOverride
+ typeAB := schema.ExperimentTypeAB
+ typeSwitchback := schema.ExperimentTypeSwitchback
+ version := int64(2)
+ startTime := time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC)
+ endTime := time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC)
+ createdAt := time.Date(2020, 1, 1, 2, 3, 4, 0, time.UTC)
+ updatedAt := time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC)
traffic100 := int32(100)
interval := int32(60)
segmentersType := map[string]schema.SegmenterType{
@@ -78,6 +92,7 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) {
pubsubCfg, _ := structpb.NewStruct(map[string]interface{}{
"key": "value",
})
+
tests := []struct {
Name string
Experiment schema.Experiment
@@ -87,14 +102,14 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) {
{
Name: "active default a/b experiment",
Experiment: schema.Experiment{
- ProjectId: 1,
- Id: 2,
- Name: "experiment-1",
- Segment: schema.ExperimentSegment{
+ ProjectId: &projectId,
+ Id: &id,
+ Name: &name,
+ Segment: &schema.ExperimentSegment{
"string_segmenter": []interface{}{"ID"},
},
- Status: schema.ExperimentStatusActive,
- Treatments: []schema.ExperimentTreatment{
+ Status: &statusActive,
+ Treatments: &[]schema.ExperimentTreatment{
{
Name: "default",
Configuration: map[string]interface{}{
@@ -103,13 +118,13 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) {
Traffic: &traffic100,
},
},
- Tier: schema.ExperimentTierDefault,
- Type: schema.ExperimentTypeAB,
- StartTime: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC),
- EndTime: time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC),
- CreatedAt: time.Date(2020, 1, 1, 2, 3, 4, 0, time.UTC),
- UpdatedAt: time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC),
- Version: 2,
+ Tier: &tierDefault,
+ Type: &typeAB,
+ StartTime: &startTime,
+ EndTime: &endTime,
+ CreatedAt: &createdAt,
+ UpdatedAt: &updatedAt,
+ Version: &version,
},
Expected: &pubsub.Experiment{
ProjectId: 1,
@@ -142,24 +157,24 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) {
{
Name: "inactive override switchback experiment",
Experiment: schema.Experiment{
- ProjectId: 3,
- Id: 4,
+ ProjectId: &projectId,
+ Id: &id,
Interval: &interval,
- Name: "experiment-2",
- Status: schema.ExperimentStatusInactive,
- Tier: schema.ExperimentTierOverride,
- Type: schema.ExperimentTypeSwitchback,
- StartTime: time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC),
- EndTime: time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC),
- CreatedAt: time.Date(2020, 1, 1, 2, 3, 4, 0, time.UTC),
- UpdatedAt: time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC),
- Version: 1,
+ Name: &name,
+ Status: &statusInactive,
+ Tier: &tierOverride,
+ Type: &typeSwitchback,
+ StartTime: &startTime,
+ EndTime: &endTime,
+ CreatedAt: &createdAt,
+ UpdatedAt: &updatedAt,
+ Version: &version,
},
Expected: &pubsub.Experiment{
- ProjectId: 3,
- Id: 4,
+ ProjectId: 1,
+ Id: 2,
Interval: 60,
- Name: "experiment-2",
+ Name: "experiment-1",
Segments: map[string]*_segmenters.ListSegmenterValue{},
Status: pubsub.Experiment_Inactive,
Treatments: []*pubsub.ExperimentTreatment{},
@@ -168,7 +183,7 @@ func TestOpenAPIExperimentSpecToProtobuf(t *testing.T) {
StartTime: timestamppb.New(time.Date(2021, 1, 1, 2, 3, 4, 0, time.UTC)),
EndTime: timestamppb.New(time.Date(2022, 1, 1, 2, 3, 4, 0, time.UTC)),
UpdatedAt: timestamppb.New(time.Date(2020, 2, 1, 2, 3, 4, 0, time.UTC)),
- Version: 1,
+ Version: 2,
},
},
}
diff --git a/treatment-service/testhelper/mockmanagement/api.go b/treatment-service/testhelper/mockmanagement/api.go
index 1ed094e0..752f88fc 100644
--- a/treatment-service/testhelper/mockmanagement/api.go
+++ b/treatment-service/testhelper/mockmanagement/api.go
@@ -212,6 +212,10 @@ type ListExperimentsParams struct {
// controls whether or not weak segmenter matches (experiments where the segmenter is optional) should be returned
IncludeWeakMatch *bool `json:"include_weak_match,omitempty"`
+
+ // A selector to restrict the list of returned objects by their fields. If unset, all the fields will be returned.
+ // Paginated responses will be returned if both or either of `page` and `page_size` parameters are provided.
+ Fields *[]externalRef0.ExperimentField `json:"fields,omitempty"`
}
// ListExperimentHistoryParams defines parameters for ListExperimentHistory.
@@ -525,6 +529,17 @@ func (siw *ServerInterfaceWrapper) ListExperiments(w http.ResponseWriter, r *htt
return
}
+ // ------------- Optional query parameter "fields" -------------
+ if paramValue := r.URL.Query().Get("fields"); paramValue != "" {
+
+ }
+
+ err = runtime.BindQueryParameter("form", true, false, "fields", r.URL.Query(), ¶ms.Fields)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Invalid format for parameter fields: %s", err), http.StatusBadRequest)
+ return
+ }
+
var handler = func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ListExperiments(w, r, projectId, params)
}
diff --git a/treatment-service/testhelper/mockmanagement/controller/experiment.go b/treatment-service/testhelper/mockmanagement/controller/experiment.go
index 80e86b36..4821515e 100644
--- a/treatment-service/testhelper/mockmanagement/controller/experiment.go
+++ b/treatment-service/testhelper/mockmanagement/controller/experiment.go
@@ -39,17 +39,17 @@ func (e Experiment) CreateExperiment(w http.ResponseWriter, r *http.Request, pro
return
}
experiment := schema.Experiment{
- ProjectId: projectId,
+ ProjectId: &projectId,
Description: requestBody.Description,
- EndTime: requestBody.EndTime,
+ EndTime: &requestBody.EndTime,
Interval: requestBody.Interval,
- Name: requestBody.Name,
- Segment: requestBody.Segment,
- StartTime: requestBody.StartTime,
- Status: requestBody.Status,
- Treatments: requestBody.Treatments,
- Type: requestBody.Type,
- UpdatedBy: *requestBody.UpdatedBy,
+ Name: &requestBody.Name,
+ Segment: &requestBody.Segment,
+ StartTime: &requestBody.StartTime,
+ Status: &requestBody.Status,
+ Treatments: &requestBody.Treatments,
+ Type: &requestBody.Type,
+ UpdatedBy: requestBody.UpdatedBy,
}
createdExperiment, err := e.ExperimentStore.CreateExperiment(experiment)
if err != nil {
@@ -86,7 +86,8 @@ func (e Experiment) EnableExperiment(w http.ResponseWriter, r *http.Request, pro
NotFound(w, err)
return
}
- experiment.Status = schema.ExperimentStatusActive
+ status := schema.ExperimentStatusActive
+ experiment.Status = &status
updatedExperiment, err := e.ExperimentStore.UpdateExperiment(projectId, experimentId, experiment)
if err != nil {
BadRequest(w, err)
@@ -102,7 +103,8 @@ func (e Experiment) DisableExperiment(w http.ResponseWriter, r *http.Request, pr
NotFound(w, err)
return
}
- experiment.Status = schema.ExperimentStatusInactive
+ status := schema.ExperimentStatusInactive
+ experiment.Status = &status
updatedExperiment, err := e.ExperimentStore.UpdateExperiment(projectId, experimentId, experiment)
if err != nil {
BadRequest(w, err)
diff --git a/treatment-service/testhelper/mockmanagement/server/server_test.go b/treatment-service/testhelper/mockmanagement/server/server_test.go
index 9b002c56..5c9df2fe 100644
--- a/treatment-service/testhelper/mockmanagement/server/server_test.go
+++ b/treatment-service/testhelper/mockmanagement/server/server_test.go
@@ -77,24 +77,28 @@ func newRandomProjectSettingsOfSize(size int) []schema.ProjectSettings {
func newRandomExperiment(projectId int64) schema.Experiment {
description := randomString()
+ endTime := time.Now().Add(time.Duration(24) * time.Hour)
interval := int32(rand.Int())
+ name := randomString()
+ startTime := time.Now()
daysOfWeek := []interface{}{int64(rand.Intn(7))}
hoursOfDay := []interface{}{int64(rand.Intn(24))}
s2Ids := []interface{}{int64(rand.Int())}
traffic := int32(rand.Int())
+ experimentType := newRandomExperimentType()
return schema.Experiment{
- ProjectId: projectId,
+ ProjectId: &projectId,
Description: &description,
- EndTime: time.Now().Add(time.Duration(24) * time.Hour),
+ EndTime: &endTime,
Interval: &interval,
- Name: randomString(),
- Segment: schema.ExperimentSegment{
+ Name: &name,
+ Segment: &schema.ExperimentSegment{
"days_of_week": daysOfWeek,
"hours_of_day": hoursOfDay,
"s2_ids": s2Ids,
},
- StartTime: time.Now(),
- Treatments: []schema.ExperimentTreatment{
+ StartTime: &startTime,
+ Treatments: &[]schema.ExperimentTreatment{
{
Configuration: map[string]interface{}{
"config": randomBool(),
@@ -103,7 +107,7 @@ func newRandomExperiment(projectId int64) schema.Experiment {
Traffic: &traffic,
},
},
- Type: newRandomExperimentType(),
+ Type: &experimentType,
}
}
@@ -220,13 +224,13 @@ func (suite *ManagementServiceTestSuite) TestExperimentCreation() {
updater := randomString()
body := management.CreateExperimentJSONRequestBody{
Description: expectedExperiment.Description,
- EndTime: expectedExperiment.EndTime,
+ EndTime: *expectedExperiment.EndTime,
Interval: expectedExperiment.Interval,
- Name: expectedExperiment.Name,
- Segment: expectedExperiment.Segment,
- StartTime: expectedExperiment.StartTime,
- Treatments: expectedExperiment.Treatments,
- Type: expectedExperiment.Type,
+ Name: *expectedExperiment.Name,
+ Segment: *expectedExperiment.Segment,
+ StartTime: *expectedExperiment.StartTime,
+ Treatments: *expectedExperiment.Treatments,
+ Type: *expectedExperiment.Type,
UpdatedBy: &updater,
}
response, err := suite.client.CreateExperimentWithResponse(suite.ctx, 1, body)
@@ -236,14 +240,14 @@ func (suite *ManagementServiceTestSuite) TestExperimentCreation() {
suite.Require().Equal(expectedExperiment.Name, createdExperiment.Name)
suite.Require().Len(suite.store.Experiments, 1)
suite.Require().Equal(suite.store.Experiments[0].Name, createdExperiment.Name)
- suite.Require().Equal(schema.ExperimentStatusActive, createdExperiment.Status)
+ suite.Require().Equal(schema.ExperimentStatusActive, *createdExperiment.Status)
suite.Require().Equal(expectedExperiment.Treatments, createdExperiment.Treatments)
- suite.Require().Equal(updater, createdExperiment.UpdatedBy)
+ suite.Require().Equal(updater, *createdExperiment.UpdatedBy)
publishedUpdate, err := getLastPublishedUpdate(suite.ctx, 1*time.Second, suite.subscription)
suite.Require().NoError(err)
suite.Require().NotNil(publishedUpdate.Update)
- suite.Require().Equal(expectedExperiment.Name, publishedUpdate.GetExperimentCreated().GetExperiment().Name)
+ suite.Require().Equal(*expectedExperiment.Name, publishedUpdate.GetExperimentCreated().GetExperiment().Name)
}
func (suite *ManagementServiceTestSuite) TestListExperiment() {
@@ -259,9 +263,10 @@ func (suite *ManagementServiceTestSuite) TestListExperiment() {
func (suite *ManagementServiceTestSuite) TestGetExperiment() {
projectId := suite.projectSettings[0].ProjectId
experiment := newRandomExperiment(projectId)
- experiment.Id = int64(rand.Int())
+ id := int64(rand.Int())
+ experiment.Id = &id
suite.store.Experiments = []schema.Experiment{experiment}
- response, err := suite.client.GetExperimentWithResponse(suite.ctx, projectId, experiment.Id)
+ response, err := suite.client.GetExperimentWithResponse(suite.ctx, projectId, *experiment.Id)
suite.Require().NoError(err)
suite.Require().Equal(experiment.Treatments, response.JSON200.Data.Treatments)
}
@@ -269,13 +274,14 @@ func (suite *ManagementServiceTestSuite) TestGetExperiment() {
func (suite *ManagementServiceTestSuite) TestExperimentUpdate() {
projectId := suite.projectSettings[0].ProjectId
experiment := newRandomExperiment(projectId)
- experiment.Id = int64(rand.Int())
+ id := int64(rand.Int())
+ experiment.Id = &id
suite.store.Experiments = []schema.Experiment{experiment}
newDescription := "updated"
params := management.UpdateExperimentJSONRequestBody{
Description: &newDescription,
}
- response, err := suite.client.UpdateExperimentWithResponse(suite.ctx, projectId, experiment.Id, params)
+ response, err := suite.client.UpdateExperimentWithResponse(suite.ctx, projectId, *experiment.Id, params)
suite.Require().NoError(err)
suite.Require().Equal(params.Description, response.JSON200.Data.Description)
}
@@ -309,34 +315,38 @@ func (suite *ManagementServiceTestSuite) TestUpdateProjectSettings() {
func (suite *ManagementServiceTestSuite) TestEnableExperiment() {
projectId := suite.projectSettings[0].ProjectId
experiment := newRandomExperiment(projectId)
- experiment.Id = int64(rand.Int())
- experiment.Status = schema.ExperimentStatusInactive
+ id := int64(rand.Int())
+ experiment.Id = &id
+ status := schema.ExperimentStatusInactive
+ experiment.Status = &status
suite.store.Experiments = []schema.Experiment{experiment}
- response, err := suite.client.EnableExperimentWithResponse(suite.ctx, projectId, experiment.Id)
+ response, err := suite.client.EnableExperimentWithResponse(suite.ctx, projectId, *experiment.Id)
suite.Require().NoError(err)
suite.Require().Equal(http.StatusOK, response.StatusCode())
publishedUpdate, err := getLastPublishedUpdate(suite.ctx, 1*time.Second, suite.subscription)
suite.Require().NoError(err)
suite.Require().NotNil(publishedUpdate.Update)
- suite.Require().Equal(experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id)
+ suite.Require().Equal(*experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id)
suite.Require().Equal(_pubsub.Experiment_Active, publishedUpdate.GetExperimentUpdated().GetExperiment().Status)
}
func (suite *ManagementServiceTestSuite) TestDisableExperiment() {
projectId := suite.projectSettings[0].ProjectId
experiment := newRandomExperiment(projectId)
- experiment.Id = int64(rand.Int())
- experiment.Status = schema.ExperimentStatusActive
+ id := int64(rand.Int())
+ experiment.Id = &id
+ status := schema.ExperimentStatusActive
+ experiment.Status = &status
suite.store.Experiments = []schema.Experiment{experiment}
- response, err := suite.client.DisableExperimentWithResponse(suite.ctx, projectId, experiment.Id)
+ response, err := suite.client.DisableExperimentWithResponse(suite.ctx, projectId, *experiment.Id)
suite.Require().NoError(err)
suite.Require().Equal(http.StatusOK, response.StatusCode())
publishedUpdate, err := getLastPublishedUpdate(suite.ctx, 1*time.Second, suite.subscription)
suite.Require().NoError(err)
suite.Require().NotNil(publishedUpdate.Update)
- suite.Require().Equal(experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id)
+ suite.Require().Equal(*experiment.Id, publishedUpdate.GetExperimentUpdated().GetExperiment().Id)
suite.Require().Equal(_pubsub.Experiment_Inactive, publishedUpdate.GetExperimentUpdated().GetExperiment().Status)
}
diff --git a/treatment-service/testhelper/mockmanagement/service/store.go b/treatment-service/testhelper/mockmanagement/service/store.go
index e8fc2061..78214a09 100644
--- a/treatment-service/testhelper/mockmanagement/service/store.go
+++ b/treatment-service/testhelper/mockmanagement/service/store.go
@@ -37,7 +37,7 @@ func (i *InMemoryStore) GetExperiment(projectId int64, experimentId int64) (sche
i.RLock()
defer i.RUnlock()
for _, experiment := range i.Experiments {
- if experiment.ProjectId == projectId && experiment.Id == experimentId {
+ if *experiment.ProjectId == projectId && *experiment.Id == experimentId {
return experiment, nil
}
}
@@ -52,7 +52,7 @@ func (i *InMemoryStore) ListExperiments(projectId int64, params api.ListExperime
defer i.RUnlock()
projectExperiments := make([]schema.Experiment, 0)
for _, experiment := range i.Experiments {
- if experiment.ProjectId == projectId {
+ if *experiment.ProjectId == projectId {
projectExperiments = append(projectExperiments, experiment)
}
}
@@ -62,10 +62,15 @@ func (i *InMemoryStore) ListExperiments(projectId int64, params api.ListExperime
func (i *InMemoryStore) CreateExperiment(experiment schema.Experiment) (schema.Experiment, error) {
i.Lock()
defer i.Unlock()
- experiment.Id = int64(len(i.Experiments)) + 1
- experiment.Status = schema.ExperimentStatusActive
- experiment.UpdatedAt = time.Now()
- experiment.Version = 1
+ id := int64(len(i.Experiments)) + 1
+ status := schema.ExperimentStatusActive
+ updatedAt := time.Now()
+ version := int64(1)
+
+ experiment.Id = &id
+ experiment.Status = &status
+ experiment.UpdatedAt = &updatedAt
+ experiment.Version = &version
i.Experiments = append(i.Experiments, experiment)
err := i.MessageQueue.PublishNewExperiment(experiment, i.SegmentersTypes)
@@ -79,10 +84,17 @@ func (i *InMemoryStore) CreateExperiment(experiment schema.Experiment) (schema.E
func (i *InMemoryStore) UpdateExperiment(projectId int64, experimentId int64, experiment schema.Experiment) (schema.Experiment, error) {
i.Lock()
defer i.Unlock()
- experiment.UpdatedAt = time.Now()
+ updatedAt := time.Now()
+
+ experiment.UpdatedAt = &updatedAt
for index, e := range i.Experiments {
- if experiment.ProjectId == projectId && e.Id == experimentId {
- experiment.Version = e.Version + 1
+ if experiment.ProjectId != nil && e.Id != nil && *experiment.ProjectId == projectId && *e.Id == experimentId {
+ var version int64
+ if e.Version != nil {
+ version = *e.Version
+ }
+ version = version + 1
+ experiment.Version = &version
i.Experiments[index] = experiment
}
}
diff --git a/ui/src/components/table/BasicTable.js b/ui/src/components/table/BasicTable.js
index 6a25d12e..7c7f471e 100644
--- a/ui/src/components/table/BasicTable.js
+++ b/ui/src/components/table/BasicTable.js
@@ -43,7 +43,7 @@ export const BasicTable = ({
pagination={{
pageIndex: page.index,
pageSize: page.size,
- showPerPageOptions: false,
+ showPerPageOptions: true,
totalItemCount,
}}
onChange={({ page = {} }) => onPaginationChange(page)}
diff --git a/ui/src/config.js b/ui/src/config.js
index 3d7ce133..7d667663 100644
--- a/ui/src/config.js
+++ b/ui/src/config.js
@@ -36,6 +36,7 @@ export const appConfig = {
: [{ href: "https://github.com/caraml-dev/xp", label: "XP User Guide" }],
pagination: {
defaultPageSize: 10,
+ experimentContextPageSize: 50,
},
tables: {
defaultTextSize: "s",
diff --git a/ui/src/experiments/list/ListExperimentsView.js b/ui/src/experiments/list/ListExperimentsView.js
index 512222ce..b49bb1ab 100644
--- a/ui/src/experiments/list/ListExperimentsView.js
+++ b/ui/src/experiments/list/ListExperimentsView.js
@@ -50,6 +50,7 @@ const ListExperimentsComponent = ({ projectId }) => {
query: {
page: page.index + 1,
page_size: page.size,
+ fields: ["id", "type", "name", "status_friendly", "tier", "start_time", "end_time", "updated_at"],
...getProcessedFilters(),
},
},
diff --git a/ui/src/providers/experiment/context.js b/ui/src/providers/experiment/context.js
index 39843a76..c004af5f 100644
--- a/ui/src/providers/experiment/context.js
+++ b/ui/src/providers/experiment/context.js
@@ -1,4 +1,4 @@
-import React, {useEffect, useMemo, useState} from "react";
+import React, { useMemo } from "react";
import { useXpApi } from "hooks/useXpApi";
import moment from "moment";
@@ -9,10 +9,6 @@ const ExperimentContext = React.createContext({});
export const ExperimentContextProvider = ({ projectId, children }) => {
const { appConfig } = useConfig();
- const [isAllExperimentsLoaded, setIsAllExperimentsLoaded] = useState(false);
- const [pageIndex, setPageIndex] = useState(0);
- const [allExperiments, setAllExperiments] = useState([]);
-
const { start_time, end_time } = useMemo(
() => {
let current_time = moment.utc();
@@ -23,38 +19,24 @@ export const ExperimentContextProvider = ({ projectId, children }) => {
[appConfig]
);
- const [{ data: { data: experiments, paging }, isLoaded }] = useXpApi(
+ const [{ data: { data: scheduledAndRunningExperiments }, isLoaded }] = useXpApi(
`/projects/${projectId}/experiments`,
{
query: {
start_time: start_time,
end_time: end_time,
- page: pageIndex + 1,
- page_size: appConfig.pagination.defaultPageSize,
- status: "active"
+ fields: ["id", "name","status_friendly", "treatments"],
+ status_friendly: ["running", "scheduled"]
},
},
- { data: [], paging: { total: 0 } }
+ { data: [] }
);
- useEffect(() => {
- if (isLoaded) {
- if (!!experiments && !isAllExperimentsLoaded) {
- setAllExperiments((curExperiments) => [...curExperiments, ...experiments]);
- }
- if (paging.pages > paging.page) {
- setPageIndex(paging.page);
- } else {
- setIsAllExperimentsLoaded(true);
- }
- }
- }, [isLoaded, experiments, paging, isAllExperimentsLoaded]);
-
return (
{children}
diff --git a/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js b/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js
index d81b4ce3..1033ecad 100644
--- a/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js
+++ b/ui/src/turing/components/form/standard_ensembler/LinkedRoutesTable.js
@@ -9,7 +9,6 @@ import {
EuiTextColor,
} from "@elastic/eui";
-import { getExperimentStatus } from "services/experiment/ExperimentStatus";
import { LinkedExperimentsContextMenu } from "./LinkedExperimentsContextMenu";
import ExperimentContext from "providers/experiment/context";
@@ -18,7 +17,7 @@ export const LinkedRoutesTable = ({
routes,
treatmentConfigRouteNamePath,
}) => {
- const { allExperiments, isAllExperimentsLoaded } = useContext(ExperimentContext)
+ const { scheduledAndRunningExperiments, isLoaded } = useContext(ExperimentContext)
const [routeToExperimentMappings, setRouteToExperimentMappings] = useState(routes.reduce((m, r) => {m[r.id] = {running: {}, scheduled: {}}; return m}, {}));
@@ -30,19 +29,19 @@ export const LinkedRoutesTable = ({
// reset loaded routeToExperimentMappings if treatmentConfigRouteNamePath or routes changes
useEffect(() => {
- if (isAllExperimentsLoaded) {
+ if (isLoaded) {
let newRouteToExperimentMappings = routes.reduce((m, r) => {m[r.id] = {running: {}, scheduled: {}}; return m}, {});
- for (let experiment of allExperiments) {
+ for (let experiment of scheduledAndRunningExperiments) {
for (let treatment of experiment.treatments) {
let configRouteName = getRouteName(treatment.configuration, treatmentConfigRouteNamePath);
if (typeof configRouteName === 'string' && configRouteName in newRouteToExperimentMappings) {
- newRouteToExperimentMappings[configRouteName][getExperimentStatus(experiment).label.toLowerCase()][experiment.id] = experiment;
+ newRouteToExperimentMappings[configRouteName][experiment.status_friendly][experiment.id] = experiment;
}
}
}
setRouteToExperimentMappings(newRouteToExperimentMappings);
}
- }, [treatmentConfigRouteNamePath, stringifiedRoutes, routes, isAllExperimentsLoaded, allExperiments]);
+ }, [treatmentConfigRouteNamePath, stringifiedRoutes, routes, isLoaded, scheduledAndRunningExperiments]);
const columns = [
{
@@ -99,7 +98,7 @@ export const LinkedRoutesTable = ({
},
];
- return isAllExperimentsLoaded ? (
+ return isLoaded ? (
r.id !== "")}