diff --git a/api/v1alpha1/frontend_types.go b/api/v1alpha1/frontend_types.go index 124c3716..4e09c54c 100644 --- a/api/v1alpha1/frontend_types.go +++ b/api/v1alpha1/frontend_types.go @@ -92,7 +92,7 @@ type WidgetEntry struct { Defaults WidgetDefaults `json:"defaults" yaml:"defaults"` } -type NavigationSegment struct { +type BundleSegment struct { SegmentID string `json:"segmentId" yaml:"segmentId"` // Id of the bundle to which the segment should be injected BundleID string `json:"bundleId" yaml:"bundleId"` @@ -103,6 +103,11 @@ type NavigationSegment struct { NavItems *[]ChromeNavItem `json:"navItems" yaml:"navItems"` } +type NavigationSegment struct { + SegmentID string `json:"segmentId" yaml:"segmentId"` + NavItems *[]ChromeNavItem `json:"navItems" yaml:"navItems"` +} + // FrontendSpec defines the desired state of Frontend type FrontendSpec struct { Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"` @@ -117,6 +122,7 @@ type FrontendSpec struct { Module *FedModule `json:"module,omitempty" yaml:"module,omitempty"` NavItems []*BundleNavItem `json:"navItems,omitempty" yaml:"navItems,omitempty"` // navigation segments for the frontend + BundleSegments []*BundleSegment `json:"bundleSegments,omitempty" yaml:"bundleSegments,omitempty"` NavigationSegments []*NavigationSegment `json:"navigationSegments,omitempty" yaml:"navigationSegments,omitempty"` AssetsPrefix string `json:"assetsPrefix,omitempty" yaml:"assetsPrefix,omitempty"` // Akamai cache bust opt-out @@ -201,13 +207,18 @@ type Permission struct { Args *apiextensions.JSON `json:"args,omitempty" yaml:"args,omitempty"` // TODO validate array item type (string?) } +type SegmentRef struct { + FrontendName string `json:"frontendName" yaml:"frontendName"` + SegmentID string `json:"segmentId" yaml:"segmentId"` +} + type ChromeNavItem struct { IsHidden bool `json:"isHidden,omitempty" yaml:"isHidden,omitempty"` Expandable bool `json:"expandable,omitempty" yaml:"expandable,omitempty"` Href string `json:"href,omitempty" yaml:"href,omitempty"` AppID string `json:"appId,omitempty" yaml:"appId,omitempty"` IsExternal bool `json:"isExternal,omitempty" yaml:"isExternal,omitempty"` - Title string `json:"title" yaml:"title"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` GroupID string `json:"groupId,omitempty" yaml:"groupId,omitempty"` ID string `json:"id,omitempty" yaml:"id,omitempty"` Product string `json:"product,omitempty" yaml:"product,omitempty"` @@ -222,6 +233,23 @@ type ChromeNavItem struct { // +kubebuilder:validation:Schemaless Routes []ChromeNavItem `json:"routes,omitempty" yaml:"routes,omitempty"` Permissions []Permission `json:"permissions,omitempty" yaml:"permissions,omitempty"` + SegmentRef *SegmentRef `json:"segmentRef,omitempty" yaml:"segmentRef,omitempty"` +} + +func (navItem ChromeNavItem) HasSegmentRef() bool { + return navItem.SegmentRef != nil +} + +func (navItem ChromeNavItem) IsValidNavItem() bool { + return navItem.Title != "" && navItem.Href != "" +} + +func (navItem ChromeNavItem) IsExpandable() bool { + return navItem.Expandable && navItem.Routes != nil +} + +func (navItem ChromeNavItem) IsGroup() bool { + return navItem.GroupID != "" && navItem.NavItems != nil } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/frontendenvironment_types.go b/api/v1alpha1/frontendenvironment_types.go index 7ceff5bd..8b3a24e2 100644 --- a/api/v1alpha1/frontendenvironment_types.go +++ b/api/v1alpha1/frontendenvironment_types.go @@ -33,9 +33,9 @@ type FrontendBundles struct { // The frontend bundles but with the nav items filled with chrome nav items type FrontendBundlesGenerated struct { - ID string `json:"id" yaml:"id"` - Title string `json:"title" yaml:"title"` - NavItems *[]ChromeNavItem `json:"navItems" yaml:"navItems"` + ID string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` + NavItems []ChromeNavItem `json:"navItems" yaml:"navItems"` } type FrontendServiceCategoryGroup struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ebb66322..a86586e2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -174,6 +174,32 @@ func (in *BundlePermission) DeepCopy() *BundlePermission { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BundleSegment) DeepCopyInto(out *BundleSegment) { + *out = *in + if in.NavItems != nil { + in, out := &in.NavItems, &out.NavItems + *out = new([]ChromeNavItem) + if **in != nil { + in, out := *in, *out + *out = make([]ChromeNavItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleSegment. +func (in *BundleSegment) DeepCopy() *BundleSegment { + if in == nil { + return nil + } + out := new(BundleSegment) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BundleSpec) DeepCopyInto(out *BundleSpec) { *out = *in @@ -247,6 +273,11 @@ func (in *ChromeNavItem) DeepCopyInto(out *ChromeNavItem) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.SegmentRef != nil { + in, out := &in.SegmentRef, &out.SegmentRef + *out = new(SegmentRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChromeNavItem. @@ -406,13 +437,9 @@ func (in *FrontendBundlesGenerated) DeepCopyInto(out *FrontendBundlesGenerated) *out = *in if in.NavItems != nil { in, out := &in.NavItems, &out.NavItems - *out = new([]ChromeNavItem) - if **in != nil { - in, out := *in, *out - *out = make([]ChromeNavItem, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + *out = make([]ChromeNavItem, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) } } } @@ -749,6 +776,17 @@ func (in *FrontendSpec) DeepCopyInto(out *FrontendSpec) { } } } + if in.BundleSegments != nil { + in, out := &in.BundleSegments, &out.BundleSegments + *out = make([]*BundleSegment, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(BundleSegment) + (*in).DeepCopyInto(*out) + } + } + } if in.NavigationSegments != nil { in, out := &in.NavigationSegments, &out.NavigationSegments *out = make([]*NavigationSegment, len(*in)) @@ -1036,6 +1074,21 @@ func (in *SearchEntry) DeepCopy() *SearchEntry { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SegmentRef) DeepCopyInto(out *SegmentRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SegmentRef. +func (in *SegmentRef) DeepCopy() *SegmentRef { + if in == nil { + return nil + } + out := new(SegmentRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceMonitorConfig) DeepCopyInto(out *ServiceMonitorConfig) { *out = *in diff --git a/config/crd/bases/cloud.redhat.com_bundles.yaml b/config/crd/bases/cloud.redhat.com_bundles.yaml index ed6e103a..209f67b0 100644 --- a/config/crd/bases/cloud.redhat.com_bundles.yaml +++ b/config/crd/bases/cloud.redhat.com_bundles.yaml @@ -89,10 +89,18 @@ spec: type: string routes: x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object title: type: string - required: - - title type: object type: array envName: @@ -147,10 +155,18 @@ spec: type: string routes: x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object title: type: string - required: - - title type: object required: - name diff --git a/config/crd/bases/cloud.redhat.com_frontends.yaml b/config/crd/bases/cloud.redhat.com_frontends.yaml index 5dc591c4..96c24b37 100644 --- a/config/crd/bases/cloud.redhat.com_frontends.yaml +++ b/config/crd/bases/cloud.redhat.com_frontends.yaml @@ -73,6 +73,89 @@ spec: type: array assetsPrefix: type: string + bundleSegments: + description: navigation segments for the frontend + items: + properties: + bundleId: + description: Id of the bundle to which the segment should be + injected + type: string + navItems: + items: + properties: + appId: + type: string + expandable: + type: boolean + groupId: + type: string + href: + type: string + icon: + type: string + id: + type: string + isBeta: + type: boolean + isExternal: + type: boolean + isHidden: + type: boolean + navItems: + description: kubebuilder struggles validating recursive + fields, it has to be helped a bit + x-kubernetes-preserve-unknown-fields: true + notifier: + type: string + permissions: + items: + properties: + apps: + items: + type: string + type: array + args: + x-kubernetes-preserve-unknown-fields: true + method: + type: string + required: + - method + type: object + type: array + product: + type: string + routes: + x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object + title: + type: string + type: object + type: array + position: + description: |- + A position of the segment within the bundle + 0 is the first position + The position "steps" should be at least 100 to make sure there is enough space in case some segments should be injected between existing ones + type: integer + segmentId: + type: string + required: + - bundleId + - navItems + - position + - segmentId + type: object + type: array deploymentRepo: type: string disabled: @@ -311,13 +394,8 @@ spec: type: object type: array navigationSegments: - description: navigation segments for the frontend items: properties: - bundleId: - description: Id of the bundle to which the segment should be - injected - type: string navItems: items: properties: @@ -364,24 +442,24 @@ spec: type: string routes: x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object title: type: string - required: - - title type: object type: array - position: - description: |- - A position of the segment within the bundle - 0 is the first position - The position "steps" should be at least 100 to make sure there is enough space in case some segments should be injected between existing ones - type: integer segmentId: type: string required: - - bundleId - navItems - - position - segmentId type: object type: array diff --git a/controllers/frontend_controller_suite_test.go b/controllers/frontend_controller_suite_test.go index 3f7802b5..7108ca0e 100644 --- a/controllers/frontend_controller_suite_test.go +++ b/controllers/frontend_controller_suite_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "time" crd "github.com/RedHatInsights/frontend-operator/api/v1alpha1" @@ -1438,3 +1439,105 @@ var _ = ginkgo.Describe("Service tiles", func() { }) }) }) + +var _ = ginkgo.Describe("Navigation nesting", func() { + const ( + FrontendName = "test-nested-nav" + FrontendNamespace = "default" + FrontendEnvName = "test-nested-nav-env" + + timeout = time.Second * 20 + duration = time.Second * 10 + interval = time.Millisecond * 250 + ) + ginkgo.It("Should stop navigation nesting if the limit is exceeded", func() { + ctx := context.Background() + configMapLookupKey := types.NamespacedName{Name: FrontendEnvName, Namespace: FrontendNamespace} + frontendEnvironment := mockFrontendEnv(FrontendEnvName, FrontendNamespace) + frontendEnvironment.Spec.Bundles = &[]crd.FrontendBundles{ + { + ID: "nested-bundle", + Title: "Nested Bundle", + }, + } + frontend := &crd.Frontend{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "cloud.redhat.com/v1", + Kind: "Frontend", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: FrontendName, + Namespace: FrontendNamespace, + }, + Spec: crd.FrontendSpec{ + EnvName: FrontendEnvName, + Title: "", + DeploymentRepo: "", + API: &crd.APIInfo{ + Versions: []string{"v1"}, + }, + Frontend: crd.FrontendInfo{ + Paths: []string{"/things/test"}, + }, + Image: "my-image:version", + Module: &crd.FedModule{ + ManifestLocation: "/apps/inventory/fed-mods.json", + Modules: []crd.Module{{ + ID: "test", + Module: "./RootApp", + Routes: []crd.Route{{ + Pathname: "/test/href", + }}, + Dependencies: []string{"depstring"}, + }}, + }, + FeoConfigEnabled: true, + // deliberately create a circular references to test the depth limit + NavigationSegments: []*crd.NavigationSegment{{ + SegmentID: "first-segment", + NavItems: &[]crd.ChromeNavItem{{ + SegmentRef: &crd.SegmentRef{ + FrontendName: FrontendName, + SegmentID: "second-segment", + }, + }}, + }, { + SegmentID: "second-segment", + NavItems: &[]crd.ChromeNavItem{{ + SegmentRef: &crd.SegmentRef{ + FrontendName: FrontendName, + SegmentID: "first-segment", + }, + }}, + }}, + BundleSegments: []*crd.BundleSegment{{ + SegmentID: "test", + BundleID: "nested-bundle", + Position: 100, + NavItems: &[]crd.ChromeNavItem{{ + SegmentRef: &crd.SegmentRef{ + FrontendName: FrontendName, + SegmentID: "first-segment", + }, + }}, + }}, + }, + } + gomega.Expect(k8sClient.Create(ctx, frontendEnvironment)).Should(gomega.Succeed()) + gomega.Expect(k8sClient.Create(ctx, frontend)).Should(gomega.Succeed()) + createdConfigMap := &v1.ConfigMap{} + var depthError error + gomega.Eventually(func() string { + err := k8sClient.Get(ctx, configMapLookupKey, createdConfigMap) + k8sClient.Get(ctx, configMapLookupKey, createdConfigMap) + if err != nil { + if strings.Contains(err.Error(), `configmaps "test-nested-nav-env" not found`) { + depthError = err + return depthError.Error() + } + return "" + } + return "" + }, timeout, interval).Should(gomega.Equal(`configmaps "test-nested-nav-env" not found`)) + }) +}) diff --git a/controllers/reconcile.go b/controllers/reconcile.go index 565ca579..a9937fb2 100644 --- a/controllers/reconcile.go +++ b/controllers/reconcile.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "slices" "sort" "strings" @@ -992,20 +993,120 @@ func getNavItemPath(feName string, bundleID string, segmentID string) string { return fmt.Sprintf("%s-%s-%s", feName, bundleID, segmentID) } -func setupBundlesData(feList *crd.FrontendList, feEnvironment crd.FrontendEnvironment) ([]crd.FrontendBundlesGenerated, []string) { +type NavSegmentCacheEntry struct { + NavItems []crd.ChromeNavItem + IsFilled bool +} + +func fillNavRefsTree(navItems []crd.ChromeNavItem, navSegmentsCache map[string]map[string]*NavSegmentCacheEntry, depth uint) ([]crd.ChromeNavItem, error) { + parsedNavItems := []crd.ChromeNavItem{} + // prevent infinite recursion and navigation nesting + // currently max known depth is 2, so 10 should be more than enough for ever + // event the depth 2 is challenging when it comes to UX, this should prevent infinite reference loops + if depth > 10 { + return parsedNavItems, fmt.Errorf("maximum navigation depth reached") + } + parsedNavItems = navItems + var err error + for index := 0; index < len(parsedNavItems); index++ { + navItem := parsedNavItems[index] + if navItem.HasSegmentRef() { + segmentRef := navItem.SegmentRef + segmentRefCacheEntry, ok := navSegmentsCache[segmentRef.FrontendName][segmentRef.SegmentID] + if !ok { + // skip if segment ref does not exist + continue + } + segmentRefItems := segmentRefCacheEntry.NavItems + // pre-fill the cache for the segment to make the next pass quicker + if !segmentRefCacheEntry.IsFilled { + segmentRefItems, err = fillNavRefsTree(segmentRefItems, navSegmentsCache, depth+1) + if err != nil { + return parsedNavItems, err + } + // don't forget to mark the segment as filled and fill it + navSegmentsCache[segmentRef.FrontendName][segmentRef.SegmentID].IsFilled = true + navSegmentsCache[segmentRef.FrontendName][segmentRef.SegmentID].NavItems = segmentRefItems + } + // delete the original ref and replace it with the filled segments + parsedNavItems = append(parsedNavItems[:index], parsedNavItems[index+1:]...) + parsedNavItems = slices.Insert(parsedNavItems, index, segmentRefItems...) + } + + // Make sure nested nav items have their refs filled as well + if navItem.IsExpandable() { + parsedRoutes, err := fillNavRefsTree(navItem.Routes, navSegmentsCache, depth+1) + if err != nil { + return parsedNavItems, err + } + parsedNavItems[index].Routes = parsedRoutes + } + + if navItem.IsGroup() { + parsedGroupItems, err := fillNavRefsTree(navItem.NavItems, navSegmentsCache, depth+1) + if err != nil { + return parsedNavItems, err + } + parsedNavItems[index].NavItems = parsedGroupItems + } + } + + return parsedNavItems, nil +} + +// Remove segment refs from nav items before they are emitted to bundles +func filterUnknownNavRefs(navItems []crd.ChromeNavItem) []crd.ChromeNavItem { + res := []crd.ChromeNavItem{} + for _, navItem := range navItems { + if navItem.HasSegmentRef() { + // skip if segment is a ref + continue + } + + if navItem.IsExpandable() { + navItem.Routes = filterUnknownNavRefs(navItem.Routes) + } + + if navItem.IsGroup() { + navItem.NavItems = filterUnknownNavRefs(navItem.NavItems) + } + + res = append(res, navItem) + } + return res +} + +func setupBundlesData(feList *crd.FrontendList, feEnvironment crd.FrontendEnvironment) ([]crd.FrontendBundlesGenerated, []string, error) { bundles := []crd.FrontendBundlesGenerated{} if feEnvironment.Spec.Bundles == nil { // skip if we do not have bundles in fe environment - return bundles, []string{} + return bundles, []string{}, nil } - skippedNavItemsMap := make(map[string][]string) - bundleNavSegmentMap := make(map[string][]crd.NavigationSegment) + // fill the nav segment cache + navSegmentsCache := make(map[string]map[string]*NavSegmentCacheEntry) for _, frontend := range feList.Items { - if frontend.Spec.FeoConfigEnabled && frontend.Spec.NavigationSegments != nil { + if frontend.Spec.NavigationSegments != nil { + // Create empty map for a frontend if not yet in cache + if _, ok := navSegmentsCache[frontend.Name]; !ok { + navSegmentsCache[frontend.Name] = make(map[string]*NavSegmentCacheEntry) + } for _, navSegment := range frontend.Spec.NavigationSegments { - bundleNavSegmentMap[navSegment.BundleID] = append(bundleNavSegmentMap[navSegment.BundleID], *navSegment) - skippedNavItemsMap[navSegment.BundleID] = append(skippedNavItemsMap[navSegment.BundleID], getNavItemPath(frontend.Name, navSegment.BundleID, navSegment.SegmentID)) + navSegmentsCache[frontend.Name][navSegment.SegmentID] = &NavSegmentCacheEntry{ + IsFilled: false, + NavItems: *navSegment.NavItems, + } + } + } + } + + skippedNavItemsMap := make(map[string][]string) + bundleNavSegmentMap := make(map[string][]crd.BundleSegment) + for _, frontend := range feList.Items { + if frontend.Spec.FeoConfigEnabled && frontend.Spec.BundleSegments != nil { + for _, bundleNavSegment := range frontend.Spec.BundleSegments { + bundleNavSegmentMap[bundleNavSegment.BundleID] = append(bundleNavSegmentMap[bundleNavSegment.BundleID], *bundleNavSegment) + skippedNavItemsMap[bundleNavSegment.BundleID] = append(skippedNavItemsMap[bundleNavSegment.BundleID], getNavItemPath(frontend.Name, bundleNavSegment.BundleID, bundleNavSegment.SegmentID)) } } } @@ -1022,10 +1123,17 @@ func setupBundlesData(feList *crd.FrontendList, feEnvironment crd.FrontendEnviro for _, navSegment := range bundleNavSegmentMap[bundle.ID] { navItems = append(navItems, *navSegment.NavItems...) } + // fill the nav refs before adding the bundle + navItems, err := fillNavRefsTree(navItems, navSegmentsCache, 0) + if err != nil { + return bundles, []string{}, err + } + + navItems = filterUnknownNavRefs(navItems) newBundle := crd.FrontendBundlesGenerated{ ID: bundle.ID, Title: bundle.Title, - NavItems: &navItems, + NavItems: navItems, } bundles = append(bundles, newBundle) } @@ -1035,7 +1143,7 @@ func setupBundlesData(feList *crd.FrontendList, feEnvironment crd.FrontendEnviro skippedNavItems = append(skippedNavItems, skipped...) } - return bundles, skippedNavItems + return bundles, skippedNavItems, nil } func (r *FrontendReconciliation) setupBundleData(_ *v1.ConfigMap, _ map[string]crd.Frontend) error { @@ -1177,7 +1285,10 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa serviceCategories, skippedTiles := setupServiceTilesData(feList, *r.FrontendEnvironment) - bundles, skippedBundles := setupBundlesData(feList, *r.FrontendEnvironment) + bundles, skippedBundles, err := setupBundlesData(feList, *r.FrontendEnvironment) + if err != nil { + return err + } fedModulesJSONData, err := json.Marshal(fedModules) if err != nil { @@ -1211,7 +1322,7 @@ func (r *FrontendReconciliation) populateConfigMap(cfgMap *v1.ConfigMap, cacheMa } if len(skippedBundles) > 0 { - r.Log.Info("Unable to find bundle for nav items:", "skippedBundles", strings.Join(skippedBundles, ",")) + r.Log.Info(fmt.Sprintf("Unable to find bundle for nav items: %s", strings.Join(skippedBundles, ","))) } cfgMap.Data["fed-modules.json"] = string(fedModulesJSONData) diff --git a/deploy.yml b/deploy.yml index 92b678ad..27702299 100644 --- a/deploy.yml +++ b/deploy.yml @@ -108,10 +108,18 @@ objects: type: string routes: x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object title: type: string - required: - - title type: object type: array envName: @@ -166,10 +174,18 @@ objects: type: string routes: x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object title: type: string - required: - - title type: object required: - name @@ -490,6 +506,92 @@ objects: type: array assetsPrefix: type: string + bundleSegments: + description: navigation segments for the frontend + items: + properties: + bundleId: + description: Id of the bundle to which the segment should + be injected + type: string + navItems: + items: + properties: + appId: + type: string + expandable: + type: boolean + groupId: + type: string + href: + type: string + icon: + type: string + id: + type: string + isBeta: + type: boolean + isExternal: + type: boolean + isHidden: + type: boolean + navItems: + description: kubebuilder struggles validating recursive + fields, it has to be helped a bit + x-kubernetes-preserve-unknown-fields: true + notifier: + type: string + permissions: + items: + properties: + apps: + items: + type: string + type: array + args: + x-kubernetes-preserve-unknown-fields: true + method: + type: string + required: + - method + type: object + type: array + product: + type: string + routes: + x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object + title: + type: string + type: object + type: array + position: + description: 'A position of the segment within the bundle + + 0 is the first position + + The position "steps" should be at least 100 to make sure + there is enough space in case some segments should be injected + between existing ones' + type: integer + segmentId: + type: string + required: + - bundleId + - navItems + - position + - segmentId + type: object + type: array deploymentRepo: type: string disabled: @@ -729,13 +831,8 @@ objects: type: object type: array navigationSegments: - description: navigation segments for the frontend items: properties: - bundleId: - description: Id of the bundle to which the segment should - be injected - type: string navItems: items: properties: @@ -782,27 +879,24 @@ objects: type: string routes: x-kubernetes-preserve-unknown-fields: true + segmentRef: + properties: + frontendName: + type: string + segmentId: + type: string + required: + - frontendName + - segmentId + type: object title: type: string - required: - - title type: object type: array - position: - description: 'A position of the segment within the bundle - - 0 is the first position - - The position "steps" should be at least 100 to make sure - there is enough space in case some segments should be injected - between existing ones' - type: integer segmentId: type: string required: - - bundleId - navItems - - position - segmentId type: object type: array diff --git a/docs/antora/modules/ROOT/pages/api_reference.adoc b/docs/antora/modules/ROOT/pages/api_reference.adoc index 8ec8fd46..d56e83d8 100644 --- a/docs/antora/modules/ROOT/pages/api_reference.adoc +++ b/docs/antora/modules/ROOT/pages/api_reference.adoc @@ -227,7 +227,7 @@ BundleSpec defines the desired state of Bundle - xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-chromenavitem[$$ChromeNavItem$$] - xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-computedbundle[$$ComputedBundle$$] - xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-extranavitem[$$ExtraNavItem$$] -- xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-navigationsegment[$$NavigationSegment$$] +- xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-navigationsegment[$$BundleSegment$$] **** [cols="20a,50a,15a,15a", options="header"] @@ -615,7 +615,7 @@ FrontendSpec defines the desired state of Frontend | *`serviceMonitor`* __xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-servicemonitorconfig[$$ServiceMonitorConfig$$]__ | | | | *`module`* __xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-fedmodule[$$FedModule$$]__ | | | | *`navItems`* __xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-bundlenavitem[$$BundleNavItem$$] array__ | | | -| *`navigationSegments`* __xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-navigationsegment[$$NavigationSegment$$] array__ | navigation segments for the frontend + | | +| *`navigationSegments`* __xref:{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-navigationsegment[$$BundleSegment$$] array__ | navigation segments for the frontend + | | | *`assetsPrefix`* __string__ | | | | *`akamaiCacheBustDisable`* __boolean__ | Akamai cache bust opt-out + | | | *`akamaiCacheBustPaths`* __string array__ | Files to cache bust + | | @@ -731,7 +731,7 @@ FrontendSpec defines the desired state of Frontend [id="{anchor_prefix}-github-com-redhatinsights-frontend-operator-api-v1alpha1-navigationsegment"] -==== NavigationSegment +==== BundleSegment diff --git a/examples/feenvironment.yaml b/examples/feenvironment.yaml index 63760c9c..8b82f5a4 100644 --- a/examples/feenvironment.yaml +++ b/examples/feenvironment.yaml @@ -23,9 +23,13 @@ spec: - id: iam title: IAM bundles: - - id: rhel - title: Red Hat Enterprise Linux - - id: ansible - title: Ansible - - id: settings - title: Settings + # - id: rhel + # title: Red Hat Enterprise Linux + # - id: ansible + # title: Ansible + # - id: settings + # title: Settings + # - id: insights + # title: Red Hat Enterprise Linux + - id: segmented-bundle + title: Segmented bundle diff --git a/examples/inventory.yaml b/examples/inventory.yaml index 2b878836..1bb3811c 100644 --- a/examples/inventory.yaml +++ b/examples/inventory.yaml @@ -18,6 +18,20 @@ spec: title: "Inventory" href: "/insights/inventory" product: "Red Hat Insights" + navigationSegments: + - segmentId: inventory-segment-one + navItems: + - id: inventory + title: Inventory segment + href: /inventory-foo-var + - segmentId: nested-segment-two + navItems: + - id: nested-two + title: Nested two + href: /nested/two + - id: nested-three + title: Nested three + href: /nested/three module: manifestLocation: "/apps/inventory/fed-mods.json" modules: diff --git a/examples/landing.yaml b/examples/landing.yaml index 89826760..cca9c453 100644 --- a/examples/landing.yaml +++ b/examples/landing.yaml @@ -7,6 +7,7 @@ spec: envName: env-boot title: landing deploymentRepo: http://test + feoConfigEnabled: true API: versions: - v1 @@ -77,7 +78,7 @@ spec: description: Some Iam thing icon: IAMIcon isExternal: false - navigationSegments: + bundleSegments: - segmentId: inventory-partial bundleId: insights position: 100 @@ -95,3 +96,53 @@ spec: - id: baz title: Some new link href: /baz + - segmentRef: + frontendName: landing + segmentId: landing-segment-one + - segmentRef: + frontendName: inventory + segmentId: inventory-segment-one + - segmentId: segment-one + bundleId: segmented-bundle + position: 100 + navItems: + - id: link-one + title: Link one classic + href: /link-one + - segmentRef: + segmentId: segment-two-item-one + frontendName: segmented-app-two + - id: link-four + title: Link four classic + href: /link-four + - segmentId: nested-segment + bundleId: segmented-bundle + position: 200 + navItems: + - id: expandable + title: Expandable + expandable: true + routes: + - id: nested-one + title: Nested one + href: /nested/one + - segmentRef: + segmentId: nested-segment-two + frontendName: inventory + - id: nested-four + title: Nested four + href: /nested/four + navigationSegments: + - segmentId: landing-segment-one + navItems: + - id: ssss + title: SSSS + href: /ssss + - segmentId: segment-two-item-one + navItems: + - id: link-two + title: Link two injected + href: /link-two + - id: link-three + title: Link three injected + href: /link-three diff --git a/tests/e2e/generate-bundles/01-create-resources.yaml b/tests/e2e/generate-bundles/01-create-resources.yaml index 9cddc798..f73e8166 100644 --- a/tests/e2e/generate-bundles/01-create-resources.yaml +++ b/tests/e2e/generate-bundles/01-create-resources.yaml @@ -33,7 +33,7 @@ spec: manifestLocation: /apps/landing-page/fed-mods.json modules: [] moduleID: landing-page - navigationSegments: + bundleSegments: - segmentId: inventory-last-segment bundleId: rhel position: 200 # should be last based on position values diff --git a/tests/e2e/generate-nav-with-segments/00-create-namespace.yaml b/tests/e2e/generate-nav-with-segments/00-create-namespace.yaml new file mode 100644 index 00000000..cbf64cb5 --- /dev/null +++ b/tests/e2e/generate-nav-with-segments/00-create-namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-generate-nav-segments-json +spec: + finalizers: + - kubernetes diff --git a/tests/e2e/generate-nav-with-segments/01-create-resources.yaml b/tests/e2e/generate-nav-with-segments/01-create-resources.yaml new file mode 100644 index 00000000..53dc7850 --- /dev/null +++ b/tests/e2e/generate-nav-with-segments/01-create-resources.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: FrontendEnvironment +metadata: + name: test-generate-nav-segments-json-environment +spec: + generateNavJSON: true + ssl: false + hostname: foo.redhat.com + sso: https://sso.foo.redhat.com + bundles: + - id: segmented-bundle + title: Segmented bundle +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: segmented-app-one + namespace: test-generate-nav-segments-json +spec: + feoConfigEnabled: true + frontend: + paths: + - / + deploymentRepo: https://github.com/RedHatInsights/insights-chrome + envName: test-generate-nav-segments-json-environment + image: quay.io/cloudservices/insights-chrome-frontend:720317c + module: + manifestLocation: /apps/chrome/js/fed-mods.json + title: Segmented app one + bundleSegments: + - segmentId: segment-one + bundleId: segmented-bundle + position: 100 + navItems: + - id: link-one + title: Link one classic + href: /link-one + - segmentRef: + segmentId: segment-two-item-one + frontendName: segmented-app-two + - id: link-four + title: Link four classic + href: /link-four + - segmentId: nested-segment + bundleId: segmented-bundle + position: 200 + navItems: + - id: expandable + title: Expandable + expandable: true + routes: + - id: nested-one + title: Nested one + href: /nested/one + - segmentRef: + segmentId: nested-segment-two + frontendName: segmented-app-two + - id: nested-four + title: Nested four + href: /nested/four +--- +apiVersion: cloud.redhat.com/v1alpha1 +kind: Frontend +metadata: + name: segmented-app-two + namespace: test-generate-nav-segments-json +spec: + feoConfigEnabled: true + frontend: + paths: + - /bar + deploymentRepo: https://github.com/RedHatInsights/insights-chrome + envName: test-generate-nav-segments-json-environment + image: foobar + module: + manifestLocation: /apps/chrome/js/fed-mods.json + title: Segmented app two + # does not have any segments but injects its nav segments to segmented-app-one + navigationSegments: + - segmentId: segment-two-item-one + navItems: + - id: link-two + title: Link two injected + href: /link-two + - id: link-three + title: Link three injected + href: /link-three + - segmentId: nested-segment-two + navItems: + - id: nested-two + title: Nested two + href: /nested/two + - id: nested-three + title: Nested three + href: /nested/three diff --git a/tests/e2e/generate-nav-with-segments/02-assert.yaml b/tests/e2e/generate-nav-with-segments/02-assert.yaml new file mode 100644 index 00000000..1027368f --- /dev/null +++ b/tests/e2e/generate-nav-with-segments/02-assert.yaml @@ -0,0 +1,15 @@ +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: test-generate-nav-segments-json-environment + namespace: test-generate-nav-segments-json + labels: + frontendenv: test-generate-nav-segments-json-environment + ownerReferences: + - name: test-generate-nav-segments-json-environment +data: + fed-modules.json: >- + {"segmentedAppOne":{"manifestLocation":"/apps/chrome/js/fed-mods.json","fullProfile":false},"segmentedAppTwo":{"manifestLocation":"/apps/chrome/js/fed-mods.json","fullProfile":false}} + bundles.json: >- + [{"id":"segmented-bundle","title":"Segmented bundle","navItems":[{"href":"/link-one","title":"Link one classic","id":"link-one"},{"href":"/link-two","title":"Link two injected","id":"link-two"},{"href":"/link-three","title":"Link three injected","id":"link-three"},{"href":"/link-four","title":"Link four classic","id":"link-four"},{"expandable":true,"title":"Expandable","id":"expandable","routes":[{"href":"/nested/one","title":"Nested one","id":"nested-one"},{"href":"/nested/two","title":"Nested two","id":"nested-two"},{"href":"/nested/three","title":"Nested three","id":"nested-three"},{"href":"/nested/four","title":"Nested four","id":"nested-four"}]}]}]