From a06142207b4a9b12067840fa675441dc815bea04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20B=C5=99ezina?= Date: Tue, 3 Dec 2024 14:08:37 +0100 Subject: [PATCH] [U-3554]: allow changing navigation links (#126) --- docs/resources/betteruptime_status_page.md | 9 + internal/provider/resource_status_page.go | 159 ++++++++++++++---- .../provider/resource_status_page_test.go | 33 ++++ 3 files changed, 170 insertions(+), 31 deletions(-) diff --git a/docs/resources/betteruptime_status_page.md b/docs/resources/betteruptime_status_page.md index 986566a..c1956f3 100644 --- a/docs/resources/betteruptime_status_page.md +++ b/docs/resources/betteruptime_status_page.md @@ -40,6 +40,7 @@ https://betterstack.com/docs/uptime/api/status-pages/ - **layout** (String) Choose usual vertical layout or space-saving horizontal layout. Only applicable when design: v2. Possible values: 'vertical', 'horizontal'. - **logo_url** (String) A direct link to your company's logo. The image should be under 20MB in size. - **min_incident_length** (Number) If you don't want to display short incidents on your status page, this attribute is for you. +- **navigation_links** (Block List) Adjust the navigation links on your status page. Only applicable when design: v2. Only first 4 links considered. (see [below for nested schema](#nestedblock--navigation_links)) - **password** (String, Sensitive) Set a password of your status page (we won't store it as plaintext, promise). Required when password_enabled: true. We will set password_enabled: false automatically when you send us an empty password. - **password_enabled** (Boolean) Do you want to enable password protection on your status page? - **status_page_group_id** (Number) Set this attribute if you want to add this status page to a status page group. @@ -53,4 +54,12 @@ https://betterstack.com/docs/uptime/api/status-pages/ - **id** (String) The ID of this Status Page. - **updated_at** (String) The time when this status page was updated. + +### Nested Schema for `navigation_links` + +Required: + +- **href** (String) Href of the link. Use full URL for external links. Use `/`, `/maintenance` and `/incidents` for built-in links. +- **text** (String) Label of the link. + diff --git a/internal/provider/resource_status_page.go b/internal/provider/resource_status_page.go index 525e2ce..52f0966 100644 --- a/internal/provider/resource_status_page.go +++ b/internal/provider/resource_status_page.go @@ -190,6 +190,25 @@ var statusPageSchema = map[string]*schema.Schema{ Optional: true, Computed: true, }, + "navigation_links": { + Description: "Adjust the navigation links on your status page. Only applicable when design: v2. Only first 4 links considered.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "text": { + Description: "Label of the link.", + Type: schema.TypeString, + Required: true, + }, + "href": { + Description: "Href of the link. Use full URL for external links. Use `/`, `/maintenance` and `/incidents` for built-in links.", + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, } func newStatusPageResource() *schema.Resource { @@ -206,35 +225,41 @@ func newStatusPageResource() *schema.Resource { } } +type navigationLink struct { + Text *string `json:"text,omitempty"` + Href *string `json:"href,omitempty"` +} + type statusPage struct { - History *int `json:"history,omitempty"` - CompanyName *string `json:"company_name,omitempty"` - CompanyURL *string `json:"company_url,omitempty"` - ContactURL *string `json:"contact_url,omitempty"` - LogoURL *string `json:"logo_remote_url,omitempty"` - Timezone *string `json:"timezone,omitempty"` - Subdomain *string `json:"subdomain,omitempty"` - CustomDomain *string `json:"custom_domain,omitempty"` - MinIncidentLength *int `json:"min_incident_length,omitempty"` - Subscribable *bool `json:"subscribable,omitempty"` - HideFromSearchEngines *bool `json:"hide_from_search_engines,omitempty"` - CustomCSS *string `json:"custom_css,omitempty"` - CustomJavaScript *string `json:"custom_javascript,omitempty"` - GoogleAnalyticsID *string `json:"google_analytics_id,omitempty"` - Announcement *string `json:"announcement,omitempty"` - AnnouncementEmbedVisible *bool `json:"announcement_embed_visible,omitempty"` - AnnouncementEmbedLink *string `json:"announcement_embed_link,omitempty"` - AnnouncementEmbedCSS *string `json:"announcement_embed_css,omitempty"` - PasswordEnabled *bool `json:"password_enabled,omitempty"` - Password *string `json:"password,omitempty"` - AggregateState *string `json:"aggregate_state,omitempty"` - CreatedAt *string `json:"created_at,omitempty"` - UpdatedAt *string `json:"updated_at,omitempty"` - Design *string `json:"design,omitempty"` - Theme *string `json:"theme,omitempty"` - Layout *string `json:"layout,omitempty"` - AutomaticReports *bool `json:"automatic_reports,omitempty"` - StatusPageGroupID *int `json:"status_page_group_id,omitempty"` + History *int `json:"history,omitempty"` + CompanyName *string `json:"company_name,omitempty"` + CompanyURL *string `json:"company_url,omitempty"` + ContactURL *string `json:"contact_url,omitempty"` + LogoURL *string `json:"logo_remote_url,omitempty"` + Timezone *string `json:"timezone,omitempty"` + Subdomain *string `json:"subdomain,omitempty"` + CustomDomain *string `json:"custom_domain,omitempty"` + MinIncidentLength *int `json:"min_incident_length,omitempty"` + Subscribable *bool `json:"subscribable,omitempty"` + HideFromSearchEngines *bool `json:"hide_from_search_engines,omitempty"` + CustomCSS *string `json:"custom_css,omitempty"` + CustomJavaScript *string `json:"custom_javascript,omitempty"` + GoogleAnalyticsID *string `json:"google_analytics_id,omitempty"` + Announcement *string `json:"announcement,omitempty"` + AnnouncementEmbedVisible *bool `json:"announcement_embed_visible,omitempty"` + AnnouncementEmbedLink *string `json:"announcement_embed_link,omitempty"` + AnnouncementEmbedCSS *string `json:"announcement_embed_css,omitempty"` + PasswordEnabled *bool `json:"password_enabled,omitempty"` + Password *string `json:"password,omitempty"` + AggregateState *string `json:"aggregate_state,omitempty"` + CreatedAt *string `json:"created_at,omitempty"` + UpdatedAt *string `json:"updated_at,omitempty"` + Design *string `json:"design,omitempty"` + Theme *string `json:"theme,omitempty"` + Layout *string `json:"layout,omitempty"` + AutomaticReports *bool `json:"automatic_reports,omitempty"` + StatusPageGroupID *int `json:"status_page_group_id,omitempty"` + NavigationLinks *[]navigationLink `json:"navigation_links,omitempty"` } type statusPageHTTPResponse struct { @@ -281,12 +306,19 @@ func statusPageRef(in *statusPage) []struct { {k: "layout", v: &in.Layout}, {k: "automatic_reports", v: &in.AutomaticReports}, {k: "status_page_group_id", v: &in.StatusPageGroupID}, + {k: "navigation_links", v: &in.NavigationLinks}, } } func statusPageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var in statusPage for _, e := range statusPageRef(&in) { - load(d, e.k, e.v) + if e.k == "navigation_links" { + if err := loadNavigationLinks(d, e.v.(**[]navigationLink)); err != nil { + return diag.FromErr(err) + } + } else { + load(d, e.k, e.v) + } } var out statusPageHTTPResponse if err := resourceCreate(ctx, meta, "/api/v2/status-pages", &in, &out); err != nil { @@ -309,6 +341,13 @@ func statusPageRead(ctx context.Context, d *schema.ResourceData, meta interface{ d.SetId("") // Force "create" on 404. return nil } + + if out.Data.Attributes.NavigationLinks != nil { + if err := d.Set("navigation_links", flattenNavigationLinks(out.Data.Attributes.NavigationLinks)); err != nil { + return diag.FromErr(err) + } + } + return statusPageCopyAttrs(d, &out.Data.Attributes, nil) } @@ -318,6 +357,12 @@ func statusPageCopyAttrs(d *schema.ResourceData, in *statusPage, derr diag.Diagn // Skip copying password as it's never returned from the API continue } + if e.k == "navigation_links" { + if err := d.Set("navigation_links", flattenNavigationLinks(in.NavigationLinks)); err != nil { + derr = append(derr, diag.FromErr(err)[0]) + } + continue + } if err := d.Set(e.k, reflect.Indirect(reflect.ValueOf(e.v)).Interface()); err != nil { derr = append(derr, diag.FromErr(err)[0]) } @@ -329,8 +374,14 @@ func statusPageUpdate(ctx context.Context, d *schema.ResourceData, meta interfac var in statusPage var out policyHTTPResponse for _, e := range statusPageRef(&in) { - if d.HasChange(e.k) { - load(d, e.k, e.v) + if e.k == "navigation_links" { + if err := loadNavigationLinks(d, e.v.(**[]navigationLink)); err != nil { + return diag.FromErr(err) + } + } else { + if d.HasChange(e.k) { + load(d, e.k, e.v) + } } } return resourceUpdate(ctx, meta, fmt.Sprintf("/api/v2/status-pages/%s", url.PathEscape(d.Id())), &in, &out) @@ -339,3 +390,49 @@ func statusPageUpdate(ctx context.Context, d *schema.ResourceData, meta interfac func statusPageDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { return resourceDelete(ctx, meta, fmt.Sprintf("/api/v2/status-pages/%s", url.PathEscape(d.Id()))) } + +func loadNavigationLinks(d *schema.ResourceData, target **[]navigationLink) error { + v, ok := d.GetOk("navigation_links") + if !ok { + return nil + } + + links := v.([]interface{}) + result := make([]navigationLink, len(links)) + + for i, link := range links { + linkMap := link.(map[string]interface{}) + + text := linkMap["text"].(string) + href := linkMap["href"].(string) + + result[i] = navigationLink{ + Text: &text, + Href: &href, + } + } + + *target = &result + return nil +} + +func flattenNavigationLinks(links *[]navigationLink) []interface{} { + if links == nil { + return nil + } + + result := make([]interface{}, len(*links)) + for i, link := range *links { + m := make(map[string]interface{}) + + if link.Text != nil { + m["text"] = *link.Text + } + if link.Href != nil { + m["href"] = *link.Href + } + + result[i] = m + } + return result +} diff --git a/internal/provider/resource_status_page_test.go b/internal/provider/resource_status_page_test.go index 1d914f6..ab79936 100644 --- a/internal/provider/resource_status_page_test.go +++ b/internal/provider/resource_status_page_test.go @@ -37,6 +37,15 @@ func TestResourceStatusPage(t *testing.T) { subdomain = "%s" password = "secret123" automatic_reports = true + + navigation_links { + text = "Example" + href = "https://example.com" + } + navigation_links { + text = "Status" + href = "/status" + } } `, subdomain), Check: resource.ComposeTestCheckFunc( @@ -45,6 +54,10 @@ func TestResourceStatusPage(t *testing.T) { resource.TestCheckResourceAttr("betteruptime_status_page.this", "timezone", "UTC"), resource.TestCheckResourceAttr("betteruptime_status_page.this", "password", "secret123"), resource.TestCheckResourceAttr("betteruptime_status_page.this", "automatic_reports", "true"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.0.text", "Example"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.0.href", "https://example.com"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.1.text", "Status"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.1.href", "/status"), ), }, // Step 2 - update. @@ -60,6 +73,14 @@ func TestResourceStatusPage(t *testing.T) { timezone = "America/Los_Angeles" subdomain = "%s" password = "secret1234" + navigation_links { + text = "Example2" + href = "https://example.com/test" + } + navigation_links { + text = "Status" + href = "/status" + } } `, subdomain), Check: resource.ComposeTestCheckFunc( @@ -67,6 +88,10 @@ func TestResourceStatusPage(t *testing.T) { resource.TestCheckResourceAttr("betteruptime_status_page.this", "subdomain", subdomain), resource.TestCheckResourceAttr("betteruptime_status_page.this", "timezone", "America/Los_Angeles"), resource.TestCheckResourceAttr("betteruptime_status_page.this", "password", "secret1234"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.0.text", "Example2"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.0.href", "https://example.com/test"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.1.text", "Status"), + resource.TestCheckResourceAttr("betteruptime_status_page.this", "navigation_links.1.href", "/status"), ), }, // Step 3 - make no changes, check plan is empty. @@ -82,6 +107,14 @@ func TestResourceStatusPage(t *testing.T) { timezone = "America/Los_Angeles" subdomain = "%s" password = "secret1234" + navigation_links { + text = "Example2" + href = "https://example.com/test" + } + navigation_links { + text = "Status" + href = "/status" + } } `, subdomain), PlanOnly: true,