diff --git a/docs/data-sources/view.md b/docs/data-sources/view.md
new file mode 100644
index 000000000..aa03569c2
--- /dev/null
+++ b/docs/data-sources/view.md
@@ -0,0 +1,77 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coralogix_view Data Source - terraform-provider-coralogix"
+subcategory: ""
+description: |-
+
+---
+
+# coralogix_view (Data Source)
+
+
+
+
+
+
+## Schema
+
+### Required
+
+- `id` (String) id
+
+### Read-Only
+
+- `filters` (Attributes) (see [below for nested schema](#nestedatt--filters))
+- `folder_id` (String) Unique identifier for folders
+- `name` (String) View name
+- `search_query` (Attributes) (see [below for nested schema](#nestedatt--search_query))
+- `time_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection))
+
+
+### Nested Schema for `filters`
+
+Read-Only:
+
+- `filters` (Attributes List) (see [below for nested schema](#nestedatt--filters--filters))
+
+
+### Nested Schema for `filters.filters`
+
+Read-Only:
+
+- `name` (String) Filter name
+- `selected_values` (Map of Boolean) Filter selected values
+
+
+
+
+### Nested Schema for `search_query`
+
+Read-Only:
+
+- `query` (String)
+
+
+
+### Nested Schema for `time_selection`
+
+Read-Only:
+
+- `custom_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--custom_selection))
+- `quick_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--quick_selection))
+
+
+### Nested Schema for `time_selection.custom_selection`
+
+Read-Only:
+
+- `from_time` (String)
+- `to_time` (String)
+
+
+
+### Nested Schema for `time_selection.quick_selection`
+
+Read-Only:
+
+- `seconds` (Number) Folder name
diff --git a/docs/data-sources/views_folder.md b/docs/data-sources/views_folder.md
new file mode 100644
index 000000000..aff8d4ee2
--- /dev/null
+++ b/docs/data-sources/views_folder.md
@@ -0,0 +1,24 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coralogix_views_folder Data Source - terraform-provider-coralogix"
+subcategory: ""
+description: |-
+
+---
+
+# coralogix_views_folder (Data Source)
+
+
+
+
+
+
+## Schema
+
+### Required
+
+- `id` (String) id
+
+### Read-Only
+
+- `name` (String) Name of the views-folder
diff --git a/docs/resources/view.md b/docs/resources/view.md
new file mode 100644
index 000000000..f03f867bd
--- /dev/null
+++ b/docs/resources/view.md
@@ -0,0 +1,81 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coralogix_view Resource - terraform-provider-coralogix"
+subcategory: ""
+description: |-
+
+---
+
+# coralogix_view (Resource)
+
+
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) View name
+- `time_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection))
+
+### Optional
+
+- `filters` (Attributes) (see [below for nested schema](#nestedatt--filters))
+- `folder_id` (String) Unique identifier for folders
+- `search_query` (Attributes) (see [below for nested schema](#nestedatt--search_query))
+
+### Read-Only
+
+- `id` (String) id
+
+
+### Nested Schema for `time_selection`
+
+Optional:
+
+- `custom_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--custom_selection))
+- `quick_selection` (Attributes) (see [below for nested schema](#nestedatt--time_selection--quick_selection))
+
+
+### Nested Schema for `time_selection.custom_selection`
+
+Required:
+
+- `from_time` (String)
+- `to_time` (String)
+
+
+
+### Nested Schema for `time_selection.quick_selection`
+
+Required:
+
+- `seconds` (Number) Folder name
+
+
+
+
+### Nested Schema for `filters`
+
+Optional:
+
+- `filters` (Attributes List) (see [below for nested schema](#nestedatt--filters--filters))
+
+
+### Nested Schema for `filters.filters`
+
+Required:
+
+- `name` (String) Filter name
+- `selected_values` (Map of Boolean) Filter selected values
+
+
+
+
+### Nested Schema for `search_query`
+
+Required:
+
+- `query` (String)
diff --git a/docs/resources/views_folder.md b/docs/resources/views_folder.md
new file mode 100644
index 000000000..bb4058963
--- /dev/null
+++ b/docs/resources/views_folder.md
@@ -0,0 +1,24 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coralogix_views_folder Resource - terraform-provider-coralogix"
+subcategory: ""
+description: |-
+
+---
+
+# coralogix_views_folder (Resource)
+
+
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) Name of the views-folder
+
+### Read-Only
+
+- `id` (String) id
diff --git a/examples/resources/coralogix_view/main.tf b/examples/resources/coralogix_view/main.tf
new file mode 100644
index 000000000..93e4376fa
--- /dev/null
+++ b/examples/resources/coralogix_view/main.tf
@@ -0,0 +1,44 @@
+terraform {
+ required_providers {
+ coralogix = {
+ version = "~> 2.0"
+ source = "coralogix/coralogix"
+ }
+ }
+}
+
+provider "coralogix" {
+ #api_key = ""
+ #env = ""
+}
+
+resource "coralogix_view" "example_view" {
+ name = "Example View"
+ time_selection = {
+ custom_selection = {
+ from_time = "2023-01-01T00:00:00Z"
+ to_time = "2023-01-02T00:00:00Z"
+ }
+ }
+ search_query = {
+ query = "error OR warning"
+ }
+ filters = {
+ filters = [
+ {
+ name = "severity"
+ selected_values = {
+ "ERROR" = true
+ "WARNING" = true
+ }
+ },
+ {
+ name = "application"
+ selected_values = {
+ "my-app" = true
+ "another-app" = true
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/resources/coralogix_views_folder/main.tf b/examples/resources/coralogix_views_folder/main.tf
new file mode 100644
index 000000000..dcd3ee6d9
--- /dev/null
+++ b/examples/resources/coralogix_views_folder/main.tf
@@ -0,0 +1,27 @@
+terraform {
+ required_providers {
+ coralogix = {
+ version = "~> 2.0"
+ source = "coralogix/coralogix"
+ }
+ }
+}
+
+provider "coralogix" {
+ #api_key = ""
+ #env = ""
+}
+
+resource "coralogix_views_folder" "example_view_folder" {
+ name = "Example View Folder"
+}
+
+resource "coralogix_view" "example_view" {
+ name = "Example View"
+ time_selection = {
+ quick_selection = {
+ seconds = 3600
+ }
+ }
+ folder_id = coralogix_views_folder.example_view_folder.id
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index f2bc2b692..f8e84f97b 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ replace github.com/grpc-ecosystem/grpc-gateway/v2 => github.com/coralogix/grpc-g
require (
github.com/ahmetalpbalkan/go-linq v3.0.0+incompatible
- github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251016142149-cfbf6680f3aa
+ github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251020091618-075a0c2c07d9
github.com/google/uuid v1.6.0
github.com/grafana/grafana-api-golang-client v0.27.0
github.com/hashicorp/terraform-plugin-docs v0.20.1
diff --git a/go.sum b/go.sum
index 4369a3072..d521a1bc7 100644
--- a/go.sum
+++ b/go.sum
@@ -38,12 +38,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251013075013-a70c0bbaecaf h1:TnlB5YHvpQctMhhh2/eqCFnxz8qr9nfzOPI31ESeuUM=
-github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251013075013-a70c0bbaecaf/go.mod h1:rxWZfvnH6aXq2zZildOH98EzTLrzQIPSKffz4kf8bkY=
-github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251015105058-3e0ead18dfb8 h1:tdoMTI+K5c79TPh7TaMQ1EPJeK66p7zSZIMaIf5f1eU=
-github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251015105058-3e0ead18dfb8/go.mod h1:rxWZfvnH6aXq2zZildOH98EzTLrzQIPSKffz4kf8bkY=
-github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251016142149-cfbf6680f3aa h1:e4ZtOfkzOtY4+ZkRA2wmTG5+GgyNm40VxTW8N+nbYV0=
-github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251016142149-cfbf6680f3aa/go.mod h1:Q7eSBDvWFb6JNfigQXGoWO3XIs6ap1CXfElaZeUrZec=
+github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251020091618-075a0c2c07d9 h1:aPHnQzRcxUfg8A8ch9bbhl6EA7TAO8ckPivp/Lw25lA=
+github.com/coralogix/coralogix-management-sdk v1.9.3-0.20251020091618-075a0c2c07d9/go.mod h1:Q7eSBDvWFb6JNfigQXGoWO3XIs6ap1CXfElaZeUrZec=
github.com/coralogix/grpc-gateway/v2 v2.0.0-20251015134251-4d8694a21a7c h1:aOfG9Pwe7Fp/m+tdybObODwgeK5st/+9df/QUw0dzBQ=
github.com/coralogix/grpc-gateway/v2 v2.0.0-20251015134251-4d8694a21a7c/go.mod h1:bqGO/kNOHTFiDIfPmXLQcox10aWQs5Q3b9sXUFoaFFk=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
diff --git a/internal/clientset/clientset.go b/internal/clientset/clientset.go
index 2d0209475..b4781b379 100644
--- a/internal/clientset/clientset.go
+++ b/internal/clientset/clientset.go
@@ -21,6 +21,7 @@ import (
ipaccess "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/ip_access_service"
cxsdkOpenapi "github.com/coralogix/coralogix-management-sdk/go/openapi/cxsdk"
+ views "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/views_service"
cxsdk "github.com/coralogix/coralogix-management-sdk/go"
)
@@ -51,6 +52,8 @@ type ClientSet struct {
groupGrpc *cxsdk.GroupsClient
notifications *cxsdk.NotificationsClient
ipaccess *ipaccess.IPAccessServiceAPIService
+ views *views.ViewsServiceAPIService
+ viewsFolders *cxsdk.ViewFoldersClient
grafana *GrafanaClient
groups *GroupsClient
@@ -164,6 +167,14 @@ func (c *ClientSet) LegacySLOs() *cxsdk.LegacySLOsClient {
return c.legacySlos
}
+func (c *ClientSet) Views() *views.ViewsServiceAPIService {
+ return c.views
+}
+
+func (c *ClientSet) ViewsFolders() *cxsdk.ViewFoldersClient {
+ return c.viewsFolders
+}
+
func NewClientSet(region string, apiKey string, targetUrl string) *ClientSet {
apiKeySdk := cxsdk.NewSDKCallPropertiesCreatorTerraform(strings.ToLower(region), cxsdk.NewAuthContext(apiKey, apiKey), TF_PROVIDER_VERSION)
apikeyCPC := NewCallPropertiesCreator(targetUrl, apiKey)
@@ -197,6 +208,8 @@ func NewClientSet(region string, apiKey string, targetUrl string) *ClientSet {
groupGrpc: cxsdk.NewGroupsClient(apiKeySdk),
notifications: cxsdk.NewNotificationsClient(apiKeySdk),
ipaccess: cxsdkOpenapi.NewIPAccessClient(oasTfCPC),
+ views: cxsdkOpenapi.NewViewsClient(oasTfCPC),
+ viewsFolders: cxsdk.NewViewFoldersClient(apiKeySdk),
grafana: NewGrafanaClient(apikeyCPC),
groups: NewGroupsClient(apikeyCPC),
diff --git a/internal/provider/data_exploration/data_source_coralogix_view.go b/internal/provider/data_exploration/data_source_coralogix_view.go
new file mode 100644
index 000000000..15c5545b1
--- /dev/null
+++ b/internal/provider/data_exploration/data_source_coralogix_view.go
@@ -0,0 +1,112 @@
+// Copyright 2024 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package data_exploration
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "strconv"
+
+ "github.com/coralogix/terraform-provider-coralogix/internal/clientset"
+ "github.com/coralogix/terraform-provider-coralogix/internal/utils"
+
+ views "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/views_service"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+)
+
+var _ datasource.DataSourceWithConfigure = &ViewDataSource{}
+
+func NewViewDataSource() datasource.DataSource {
+ return &ViewDataSource{}
+}
+
+type ViewDataSource struct {
+ client *views.ViewsServiceAPIService
+}
+
+func (d *ViewDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_view"
+}
+
+func (d *ViewDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ clientSet, ok := req.ProviderData.(*clientset.ClientSet)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = clientSet.Views()
+}
+
+func (d *ViewDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ var r ViewResource
+ var resourceResp resource.SchemaResponse
+ r.Schema(ctx, resource.SchemaRequest{}, &resourceResp)
+
+ resp.Schema = utils.FrameworkDatasourceSchemaFromFrameworkResourceSchema(resourceResp.Schema)
+}
+
+func (d *ViewDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data *ViewModel
+ diags := req.Config.Get(ctx, &data)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ //Get refreshed View value from Coralogix
+ idStr := data.Id.ValueString()
+ id, err := strconv.ParseInt(idStr, 10, 32)
+
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Invalid View ID",
+ fmt.Sprintf("ID '%s' is not a valid 32-bit integer: %s", idStr, err.Error()),
+ )
+ return
+ }
+ rq := d.client.ViewsServiceGetView(ctx, int32(id))
+ log.Printf("[INFO] Reading new resource: %s", utils.FormatJSON(rq))
+ result, _, err := rq.Execute()
+
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ resp.Diagnostics.AddError(
+ "Error reading View",
+ utils.FormatOpenAPIErrors(err, "Read", nil),
+ )
+ return
+ }
+ log.Printf("[INFO] Read resource: %s", utils.FormatJSON(result))
+
+ data, diags = flattenView(ctx, result)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ // Set state to fully populated data
+ diags = resp.State.Set(ctx, &data)
+ resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/provider/data_exploration/data_source_coralogix_views_folder.go b/internal/provider/data_exploration/data_source_coralogix_views_folder.go
new file mode 100644
index 000000000..605c96d74
--- /dev/null
+++ b/internal/provider/data_exploration/data_source_coralogix_views_folder.go
@@ -0,0 +1,106 @@
+// Copyright 2025 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package data_exploration
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset"
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/utils"
+
+ "google.golang.org/protobuf/types/known/wrapperspb"
+
+ cxsdk "github.com/coralogix/coralogix-management-sdk/go"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "google.golang.org/grpc/codes"
+)
+
+var _ datasource.DataSourceWithConfigure = &ViewsFolderDataSource{}
+
+func NewViewsFolderDataSource() datasource.DataSource {
+ return &ViewsFolderDataSource{}
+}
+
+type ViewsFolderDataSource struct {
+ client *cxsdk.ViewFoldersClient
+}
+
+func (d *ViewsFolderDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_views_folder"
+}
+
+func (d *ViewsFolderDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ clientSet, ok := req.ProviderData.(*clientset.ClientSet)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = clientSet.ViewsFolders()
+}
+
+func (d *ViewsFolderDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ var r ViewsFolderResource
+ var resourceResp resource.SchemaResponse
+ r.Schema(ctx, resource.SchemaRequest{}, &resourceResp)
+
+ resp.Schema = utils.FrameworkDatasourceSchemaFromFrameworkResourceSchema(resourceResp.Schema)
+}
+
+func (d *ViewsFolderDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var data *ViewsFolderModel
+ diags := req.Config.Get(ctx, &data)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ //Get refreshed Views-Folder value from Coralogix
+ id := data.Id.ValueString()
+ log.Printf("[INFO] Reading views-folder: %s", id)
+ getViewsFolderResp, err := d.client.Get(ctx, &cxsdk.GetViewFolderRequest{Id: wrapperspb.String(id)})
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ if cxsdk.Code(err) == codes.NotFound {
+ resp.Diagnostics.AddWarning(
+ err.Error(),
+ fmt.Sprintf("Views-Folder %q is in state, but no longer exists in Coralogix backend", id),
+ )
+ } else {
+ resp.Diagnostics.AddError(
+ "Error reading Views-Folder",
+ utils.FormatRpcErrors(err, fmt.Sprintf("%s/%s", cxsdk.GetViewFolderRPC, id), ""),
+ )
+ }
+ return
+ }
+ respStr, _ := json.Marshal(getViewsFolderResp)
+ log.Printf("[INFO] Received View: %s", string(respStr))
+
+ data = flattenViewsFolder(getViewsFolderResp.Folder)
+
+ // Set state to fully populated data
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_exploration/resource_coralogix_view.go b/internal/provider/data_exploration/resource_coralogix_view.go
new file mode 100644
index 000000000..db9c40a1e
--- /dev/null
+++ b/internal/provider/data_exploration/resource_coralogix_view.go
@@ -0,0 +1,876 @@
+// Copyright 2025 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package data_exploration
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "regexp"
+ "strconv"
+ "time"
+
+ views "github.com/coralogix/coralogix-management-sdk/go/openapi/gen/views_service"
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset"
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/utils"
+
+ cxsdk "github.com/coralogix/coralogix-management-sdk/go"
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/types/known/timestamppb"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+var _ resource.Resource = (*ViewResource)(nil)
+
+func NewViewResource() resource.Resource {
+ return &ViewResource{}
+}
+
+type ViewResource struct {
+ client *views.ViewsServiceAPIService
+}
+
+func (r *ViewResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_view"
+}
+
+func (r *ViewResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ clientSet, ok := req.ProviderData.(*clientset.ClientSet)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = clientSet.Views()
+}
+
+func (r *ViewResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int32Attribute{
+ Computed: true,
+ Description: "id",
+ MarkdownDescription: "id",
+ PlanModifiers: []planmodifier.Int32{
+ int32planmodifier.UseStateForUnknown(),
+ },
+ },
+ "folder_id": schema.StringAttribute{
+ Optional: true,
+ Description: "Unique identifier for folders",
+ MarkdownDescription: "Unique identifier for folders",
+ Validators: []validator.String{
+ stringvalidator.LengthBetween(36, 36),
+ stringvalidator.RegexMatches(regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"), ""),
+ },
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "View name",
+ MarkdownDescription: "View name",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "search_query": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "query": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "filters": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "filters": schema.ListNestedAttribute{
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "Filter name",
+ MarkdownDescription: "Filter name",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "selected_values": schema.MapAttribute{
+ ElementType: types.BoolType,
+ Required: true,
+ Description: "Filter selected values",
+ MarkdownDescription: "Filter selected values",
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ Validators: []validator.List{
+ listvalidator.SizeAtLeast(1),
+ },
+ PlanModifiers: []planmodifier.List{
+ listplanmodifier.UseStateForUnknown(),
+ },
+ },
+ },
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "time_selection": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "custom_selection": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "from_time": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "to_time": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ },
+ Validators: []validator.Object{
+ objectvalidator.ExactlyOneOf(
+ path.MatchRoot("time_selection").AtName("quick_selection"),
+ ),
+ },
+ Optional: true,
+ },
+ "quick_selection": schema.SingleNestedAttribute{
+ Attributes: map[string]schema.Attribute{
+ "seconds": schema.Int64Attribute{
+ Required: true,
+ Description: "Folder name",
+ MarkdownDescription: "Folder name",
+ },
+ },
+ Optional: true,
+ },
+ },
+ Required: true,
+ },
+ },
+ }
+}
+
+func (r *ViewResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ViewResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data *ViewModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Create API call logic
+ createViewRequest, diags := extractCreateView(ctx, data)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ viewStr := protojson.Format(createViewRequest)
+ log.Printf("[INFO] Creating new view: %s", viewStr)
+ createViewResponse, err := r.client.Create(ctx, createViewRequest)
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err)
+ resp.Diagnostics.AddError("Error creating View",
+ utils.FormatRpcErrors(err, cxsdk.CreateActionRPC, viewStr),
+ )
+ return
+ }
+ log.Printf("[INFO] View created successfully: %s", protojson.Format(createViewResponse.View))
+
+ // Save data into Terraform state
+ data, diags = flattenView(ctx, createViewResponse.View)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
+}
+
+func flattenView(ctx context.Context, view *views.View) (*ViewModel, diag.Diagnostics) {
+ filters, diags := flattenViewFilter(ctx, view.Filters)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ searchQuery, diags := flattenSearchQuery(ctx, view.SearchQuery)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ timeSelection, diags := flattenViewTimeSelection(ctx, &view.TimeSelection)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return &ViewModel{
+ Filters: filters,
+ FolderId: utils.WrapperspbStringToTypeString(view.FolderId),
+ Id: types.Int32Value(view.Id),
+ Name: utils.WrapperspbStringToTypeString(view.Name),
+ SearchQuery: searchQuery,
+ TimeSelection: timeSelection,
+ }, nil
+}
+
+func flattenViewTimeSelection(ctx context.Context, selection *views.TimeSelection) (types.Object, diag.Diagnostics) {
+ if selection == nil {
+ return TimeSelectionModel{
+ CustomSelection: types.ObjectNull(CustomSelectionModel{}.AttributeTypes(ctx)),
+ QuickSelection: types.ObjectNull(QuickSelectionModel{}.AttributeTypes(ctx)),
+ }.ToObjectValue(ctx)
+ }
+
+ if quickSelection := selection.GetQuickSelection(); quickSelection != nil {
+ qs, diags := flattenQuickSelection(ctx, quickSelection)
+ if diags.HasError() {
+ return types.ObjectNull(TimeSelectionModel{}.AttributeTypes(ctx)), diags
+ }
+
+ return TimeSelectionModel{
+ QuickSelection: qs,
+ CustomSelection: types.ObjectNull(CustomSelectionModel{}.AttributeTypes(ctx)),
+ }.ToObjectValue(ctx)
+ }
+
+ if customSelection := selection.GetCustomSelection(); customSelection != nil {
+ cs, diags := flattenCustomSelection(ctx, customSelection)
+ if diags.HasError() {
+ return types.ObjectNull(TimeSelectionModel{}.AttributeTypes(ctx)), diags
+ }
+ return TimeSelectionModel{
+ CustomSelection: cs,
+ QuickSelection: types.ObjectNull(QuickSelectionModel{}.AttributeTypes(ctx)),
+ }.ToObjectValue(ctx)
+ }
+
+ return types.ObjectNull(TimeSelectionModel{}.AttributeTypes(ctx)), diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid Time Selection",
+ "Time selection must have either quick selection or custom selection defined.",
+ ),
+ }
+}
+
+func flattenCustomSelection(ctx context.Context, selection *cxsdk.CustomTimeSelection) (types.Object, diag.Diagnostics) {
+ if selection == nil {
+ return types.ObjectNull(CustomSelectionModel{}.AttributeTypes(ctx)), nil
+ }
+
+ customSelectionModel := CustomSelectionModel{
+ FromTime: types.StringValue(selection.FromTime.AsTime().Format(time.RFC3339)),
+ ToTime: types.StringValue(selection.ToTime.AsTime().Format(time.RFC3339)),
+ }
+
+ return customSelectionModel.ToObjectValue(ctx)
+}
+
+func flattenQuickSelection(ctx context.Context, selection *cxsdk.QuickTimeSelection) (types.Object, diag.Diagnostics) {
+ if selection == nil {
+ return types.ObjectNull(QuickSelectionModel{}.AttributeTypes(ctx)), nil
+ }
+
+ quickSelectionModel := QuickSelectionModel{
+ Seconds: types.Int64Value(int64(selection.Seconds)),
+ }
+
+ return quickSelectionModel.ToObjectValue(ctx)
+}
+
+func flattenSearchQuery(ctx context.Context, query *views.SearchQuery) (types.Object, diag.Diagnostics) {
+ if query == nil {
+ return types.ObjectNull(SearchQueryModel{}.AttributeTypes(ctx)), nil
+ }
+
+ return SearchQueryModel{
+ Query: utils.WrapperspbStringToTypeString(query.Query),
+ }.ToObjectValue(ctx)
+}
+
+func flattenViewFilter(ctx context.Context, filters *views.SelectedFilters) (types.Object, diag.Diagnostics) {
+ if filters == nil {
+ return types.ObjectNull(FiltersModel{}.AttributeTypes()), nil
+ }
+
+ innerFilters, diags := flattenInnerViewFilters(ctx, filters.Filters)
+ if diags.HasError() {
+ return types.ObjectNull(FiltersModel{}.AttributeTypes()), diags
+ }
+
+ return FiltersModel{
+ Filters: innerFilters,
+ }.ToObjectValue(ctx)
+}
+
+func flattenInnerViewFilters(ctx context.Context, filters []views.ViewsV1Filter) (basetypes.ListValue, diag.Diagnostics) {
+ if filters == nil {
+ return types.ListNull(types.ObjectType{AttrTypes: InnerFiltersModel{}.AttributeTypes()}), nil
+ }
+
+ innerFilters := make([]InnerFiltersModel, 0, len(filters))
+ var diags diag.Diagnostics
+ for i := range filters {
+ var selectedValues basetypes.MapValue
+ if filters[i].SelectedValues == nil {
+ selectedValues = types.MapNull(types.BoolType)
+ } else {
+ var dgs diag.Diagnostics
+ selectedValues, dgs = types.MapValueFrom(ctx, types.BoolType, filters[i].SelectedValues)
+ if dgs.HasError() {
+ diags.Append(dgs...)
+ continue
+ }
+ }
+
+ innerFilters = append(innerFilters, InnerFiltersModel{
+ Name: utils.WrapperspbStringToTypeString(filters[i].Name),
+ SelectedValues: selectedValues,
+ })
+ }
+
+ if diags.HasError() {
+ return types.ListNull(types.ObjectType{AttrTypes: InnerFiltersModel{}.AttributeTypes()}), diags
+ }
+
+ return types.ListValueFrom(ctx, types.ObjectType{AttrTypes: InnerFiltersModel{}.AttributeTypes()}, innerFilters)
+}
+
+func extractCreateView(ctx context.Context, data *ViewModel) (*cxsdk.CreateViewRequest, diag.Diagnostics) {
+ filters, diags := expandSelectedFilters(ctx, data.Filters)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ timeSelection, diags := expandViewTimeSelection(ctx, data.TimeSelection)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ searchQuery, diags := expandViewSearchQuery(ctx, data.SearchQuery)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return &cxsdk.CreateViewRequest{
+ Name: utils.TypeStringToWrapperspbString(data.Name),
+ SearchQuery: searchQuery,
+ TimeSelection: timeSelection,
+ Filters: filters,
+ FolderId: utils.TypeStringToWrapperspbString(data.FolderId),
+ }, nil
+}
+
+func extractUpdateView(ctx context.Context, data *ViewModel) (*cxsdk.ReplaceViewRequest, diag.Diagnostics) {
+ filters, diags := expandSelectedFilters(ctx, data.Filters)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ timeSelection, diags := expandViewTimeSelection(ctx, data.TimeSelection)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ searchQuery, diags := expandViewSearchQuery(ctx, data.SearchQuery)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return &cxsdk.ReplaceViewRequest{
+ View: &cxsdk.View{
+ Id: wrapperspb.Int32(int32(data.Id.ValueInt32())),
+ Name: utils.TypeStringToWrapperspbString(data.Name),
+ SearchQuery: searchQuery,
+ TimeSelection: timeSelection,
+ Filters: filters,
+ FolderId: utils.TypeStringToWrapperspbString(data.FolderId),
+ },
+ }, nil
+}
+
+func expandSelectedFilters(ctx context.Context, filtersObject types.Object) (*cxsdk.SelectedFilters, diag.Diagnostics) {
+ if filtersObject.IsNull() || filtersObject.IsUnknown() {
+ return nil, nil
+ }
+
+ ov, _ := filtersObject.ToObjectValue(ctx)
+ var filters FiltersModel
+ if dg := ov.As(ctx, &filters, basetypes.ObjectAsOptions{}); dg.HasError() {
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid Filters Object",
+ fmt.Sprintf("Expected FiltersModel, got: %T. Please report this issue to the provider developers.", filtersObject),
+ ),
+ }
+ }
+ innerFilters, diags := expandViewFilters(ctx, filters.Filters)
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return &cxsdk.SelectedFilters{
+ Filters: innerFilters,
+ }, nil
+
+}
+
+func expandViewFilters(ctx context.Context, filters basetypes.ListValue) ([]*cxsdk.ViewFilter, diag.Diagnostics) {
+ if filters.IsNull() || filters.IsUnknown() {
+ return nil, nil
+ }
+
+ var diags diag.Diagnostics
+ var filtersObjects []types.Object
+ diags = filters.ElementsAs(ctx, &filtersObjects, true)
+ innerFilters := make([]*cxsdk.ViewFilter, 0, len(filtersObjects))
+ for _, fo := range filtersObjects {
+ var innerFilterValue InnerFiltersModel
+ if dg := fo.As(ctx, &innerFilterValue, basetypes.ObjectAsOptions{}); dg.HasError() {
+ diags.Append(dg...)
+ continue
+ }
+
+ var selectedValues map[string]bool
+ if dg := innerFilterValue.SelectedValues.ElementsAs(ctx, &selectedValues, true); dg.HasError() {
+ diags.Append(dg...)
+ continue
+ }
+ innerFilters = append(innerFilters, &cxsdk.ViewFilter{
+ Name: utils.TypeStringToWrapperspbString(innerFilterValue.Name),
+ SelectedValues: selectedValues,
+ })
+ }
+
+ if diags.HasError() {
+ return nil, diags
+ }
+
+ return innerFilters, nil
+}
+
+func expandViewSearchQuery(ctx context.Context, queryObject types.Object) (*cxsdk.SearchQuery, diag.Diagnostics) {
+ if queryObject.IsNull() || queryObject.IsUnknown() {
+ return nil, nil
+ }
+
+ ov, _ := queryObject.ToObjectValue(ctx)
+ var query SearchQueryModel
+ if dg := ov.As(ctx, &query, basetypes.ObjectAsOptions{}); dg.HasError() {
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid Search Query Object",
+ fmt.Sprintf("Expected SearchQueryModel, got: %T. Please report this issue to the provider developers.", queryObject),
+ ),
+ }
+ }
+
+ return &cxsdk.SearchQuery{
+ Query: utils.TypeStringToWrapperspbString(query.Query),
+ }, nil
+}
+
+func expandViewTimeSelection(ctx context.Context, selectionObject types.Object) (*cxsdk.TimeSelection, diag.Diagnostics) {
+ if selectionObject.IsNull() || selectionObject.IsUnknown() {
+ return nil, nil
+ }
+
+ var selection TimeSelectionModel
+ if dg := selectionObject.As(ctx, &selection, basetypes.ObjectAsOptions{}); dg.HasError() {
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid Time Selection Object",
+ fmt.Sprintf("Expected TimeSelectionModel, got: %T. Please report this issue to the provider developers.", selectionObject),
+ ),
+ }
+ }
+
+ if quickSelection := selection.QuickSelection; !(quickSelection.IsNull() || quickSelection.IsUnknown()) {
+ qs, diags := expandQuickSelection(ctx, selection.QuickSelection)
+ if diags.HasError() {
+ return nil, diags
+ }
+ return &cxsdk.TimeSelection{
+ SelectionType: &cxsdk.ViewTimeSelectionQuick{
+ QuickSelection: qs,
+ },
+ }, nil
+ } else if customSelection := selection.CustomSelection; !(customSelection.IsNull() || customSelection.IsUnknown()) {
+ cs, diags := expandCustomSelection(ctx, selection.CustomSelection)
+ if diags.HasError() {
+ return nil, diags
+ }
+ return &cxsdk.TimeSelection{
+ SelectionType: &cxsdk.ViewTimeSelectionCustom{
+ CustomSelection: cs,
+ },
+ }, nil
+ }
+
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid Time Selection",
+ "Time selection must have either quick selection or custom selection defined.",
+ ),
+ }
+}
+
+func expandCustomSelection(ctx context.Context, selection types.Object) (*cxsdk.CustomTimeSelection, diag.Diagnostics) {
+ if selection.IsNull() || selection.IsUnknown() {
+ return nil, nil
+ }
+
+ attributes := selection.Attributes()
+ fromTimeAttr, ok := attributes["from_time"]
+ if !ok {
+ return nil, nil
+ }
+ toTimeAttr, ok := attributes["to_time"]
+ if !ok {
+ return nil, nil
+ }
+
+ fromTime, ok := fromTimeAttr.(types.String)
+ if !ok || fromTime.IsNull() || fromTime.IsUnknown() {
+ return nil, nil
+ }
+ toTime, ok := toTimeAttr.(types.String)
+ if !ok || toTime.IsNull() || toTime.IsUnknown() {
+ return nil, nil
+ }
+
+ ft, err := time.Parse(time.RFC3339, fromTime.ValueString())
+ if err != nil {
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid From Time Format",
+ fmt.Sprintf("From time '%s' is not in RFC3339 format: %s", fromTime.ValueString(), err.Error()),
+ ),
+ }
+ }
+ tt, err := time.Parse(time.RFC3339, toTime.ValueString())
+ if err != nil {
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid To Time Format",
+ fmt.Sprintf("To time '%s' is not in RFC3339 format: %s", toTime.ValueString(), err.Error()),
+ ),
+ }
+ }
+
+ return &cxsdk.CustomTimeSelection{
+ FromTime: timestamppb.New(ft),
+ ToTime: timestamppb.New(tt),
+ }, nil
+}
+
+func expandQuickSelection(ctx context.Context, selection types.Object) (*cxsdk.QuickTimeSelection, diag.Diagnostics) {
+ if selection.IsNull() || selection.IsUnknown() {
+ return nil, nil
+ }
+
+ attributes := selection.Attributes()
+ secondsAttr, ok := attributes["seconds"]
+ if !ok {
+ return nil, nil
+ }
+
+ seconds, ok := secondsAttr.(types.Int64)
+ if !ok || seconds.IsNull() || seconds.IsUnknown() {
+ return nil, nil
+ }
+
+ if seconds.ValueInt64() < 0 {
+ return nil, diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid Seconds Value",
+ fmt.Sprintf("Seconds value '%d' cannot be negative.", seconds.ValueInt64()),
+ ),
+ }
+ }
+
+ return &cxsdk.QuickTimeSelection{
+ Seconds: uint32(seconds.ValueInt64()),
+ }, nil
+}
+
+func (r *ViewResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data *ViewModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ idStr := data.Id.ValueString()
+ id, err := strconv.Atoi(idStr)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Invalid View ID",
+ fmt.Sprintf("ID '%s' is not a valid integer: %s", idStr, err.Error()),
+ )
+ return
+ }
+
+ readReq := &cxsdk.GetViewRequest{
+ Id: wrapperspb.Int32(int32(id)),
+ }
+ log.Printf("[INFO] Reading view with ID: %s", idStr)
+ readResp, err := r.client.Get(ctx, readReq)
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ if cxsdk.Code(err) == codes.NotFound {
+ resp.Diagnostics.AddWarning(
+ fmt.Sprintf("View %q is in state, but no longer exists in Coralogix backend", idStr),
+ fmt.Sprintf("%s will be recreated when you apply", idStr),
+ )
+ resp.State.RemoveResource(ctx)
+ } else {
+ resp.Diagnostics.AddError(
+ "Error reading view",
+ utils.FormatRpcErrors(err, cxsdk.GetViewRPC, protojson.Format(readReq)),
+ )
+ }
+ return
+ }
+ log.Printf("[INFO] View read successfully: %s", protojson.Format(readResp.View))
+
+ // Flatten the response into the model
+ data, diags := flattenView(ctx, readResp.View)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ViewResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data *ViewModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Save updated data into Terraform state
+ updateReq, diags := extractUpdateView(ctx, data)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ log.Printf("[INFO] Updating view in state: %s", protojson.Format(updateReq))
+ updateResp, err := r.client.Replace(ctx, updateReq)
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ resp.Diagnostics.AddError(
+ "Error updating view in state",
+ utils.FormatRpcErrors(err, cxsdk.ReplaceViewRPC, protojson.Format(updateReq)),
+ )
+ return
+ }
+ log.Printf("[INFO] View updated in state successfully: %s", protojson.Format(updateResp.View))
+
+ // Flatten the response into the model
+ data, diags = flattenView(ctx, updateResp.View)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ViewResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data *ViewModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ id := data.Id
+ rq := r.client.ViewsServiceDeleteView(ctx, id.ValueInt32())
+ _, _, err := rq.Execute()
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ if cxsdk.Code(err) == codes.NotFound {
+ resp.Diagnostics.AddWarning(
+ fmt.Sprintf("View %q is in state, but no longer exists in Coralogix backend", id),
+ fmt.Sprintf("%d will be removed from state", id),
+ )
+ resp.State.RemoveResource(ctx)
+ } else {
+ resp.Diagnostics.AddError(
+ "Error deleting view",
+ utils.FormatRpcErrors(err, cxsdk.DeleteViewRPC, fmt.Sprintf("ID: %d", id)),
+ )
+ }
+ return
+ }
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+type ViewModel struct {
+ Filters types.Object `tfsdk:"filters"` //FiltersModel
+ FolderId types.String `tfsdk:"folder_id"`
+ Id types.Int32 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ SearchQuery types.Object `tfsdk:"search_query"` //SearchQueryModel
+ TimeSelection types.Object `tfsdk:"time_selection"` // TimeSelectionModel
+}
+
+type QuickSelectionModel struct {
+ Seconds types.Int64 `tfsdk:"seconds"`
+}
+
+func (v QuickSelectionModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v)
+}
+
+func (v QuickSelectionModel) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "seconds": basetypes.Int64Type{},
+ }
+}
+
+type FiltersModel struct {
+ Filters types.List `tfsdk:"filters"` // InnerFiltersModel
+}
+
+func (v FiltersModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, v.AttributeTypes(), v)
+}
+
+func (v FiltersModel) AttributeTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "filters": basetypes.ListType{
+ ElemType: basetypes.ObjectType{
+ AttrTypes: InnerFiltersModel{}.AttributeTypes(),
+ },
+ },
+ }
+}
+
+type InnerFiltersModel struct {
+ Name types.String `tfsdk:"name"`
+ SelectedValues types.Map `tfsdk:"selected_values"`
+}
+
+func (v InnerFiltersModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, v.AttributeTypes(), v)
+}
+
+func (v InnerFiltersModel) AttributeTypes() map[string]attr.Type {
+ return map[string]attr.Type{
+ "name": basetypes.StringType{},
+ "selected_values": basetypes.MapType{
+ ElemType: types.BoolType,
+ },
+ }
+}
+
+type SearchQueryModel struct {
+ Query types.String `tfsdk:"query"`
+}
+
+func (v SearchQueryModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v)
+}
+
+func (v SearchQueryModel) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "query": basetypes.StringType{},
+ }
+}
+
+type TimeSelectionModel struct {
+ CustomSelection types.Object `tfsdk:"custom_selection"` //CustomSelectionModel
+ QuickSelection types.Object `tfsdk:"quick_selection"` //QuickSelectionModel
+}
+
+func (v TimeSelectionModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v)
+}
+
+func (v TimeSelectionModel) AttributeTypes(ctx context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "custom_selection": basetypes.ObjectType{
+ AttrTypes: CustomSelectionModel{}.AttributeTypes(ctx),
+ },
+ "quick_selection": basetypes.ObjectType{
+ AttrTypes: QuickSelectionModel{}.AttributeTypes(ctx),
+ },
+ }
+}
+
+type CustomSelectionModel struct {
+ FromTime types.String `tfsdk:"from_time"`
+ ToTime types.String `tfsdk:"to_time"`
+}
+
+func (v CustomSelectionModel) ToObjectValue(ctx context.Context) (types.Object, diag.Diagnostics) {
+ return types.ObjectValueFrom(ctx, v.AttributeTypes(ctx), v)
+}
+
+func (v CustomSelectionModel) AttributeTypes(context.Context) map[string]attr.Type {
+ return map[string]attr.Type{
+ "from_time": basetypes.StringType{},
+ "to_time": basetypes.StringType{},
+ }
+}
diff --git a/internal/provider/data_exploration/resource_coralogix_views_folder.go b/internal/provider/data_exploration/resource_coralogix_views_folder.go
new file mode 100644
index 000000000..c0ec19f66
--- /dev/null
+++ b/internal/provider/data_exploration/resource_coralogix_views_folder.go
@@ -0,0 +1,248 @@
+// Copyright 2024 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package data_exploration
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset"
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/utils"
+
+ cxsdk "github.com/coralogix/coralogix-management-sdk/go"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+var _ resource.Resource = (*ViewsFolderResource)(nil)
+
+func NewViewsFolderResource() resource.Resource {
+ return &ViewsFolderResource{}
+}
+
+type ViewsFolderResource struct {
+ client *cxsdk.ViewFoldersClient
+}
+
+func (r *ViewsFolderResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_views_folder"
+}
+
+func (r *ViewsFolderResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ clientSet, ok := req.ProviderData.(*clientset.ClientSet)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *clientset.ClientSet, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ r.client = clientSet.ViewsFolders()
+}
+
+func (r *ViewsFolderResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = ViewsFolderResourceSchema(ctx)
+}
+
+func (r *ViewsFolderResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *ViewsFolderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data *ViewsFolderModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Create API call logic
+ createRequest := &cxsdk.CreateViewFolderRequest{
+ Name: utils.TypeStringToWrapperspbString(data.Name),
+ }
+ viewFolderStr := protojson.Format(createRequest)
+ log.Printf("[INFO] Creating new views-folder: %s", viewFolderStr)
+ createResponse, err := r.client.Create(ctx, createRequest)
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err)
+ resp.Diagnostics.AddError("Error creating Views-Folder",
+ utils.FormatRpcErrors(err, cxsdk.CreateActionRPC, viewFolderStr),
+ )
+ return
+ }
+ log.Printf("[INFO] Views-Folder created successfully: %s", protojson.Format(createResponse))
+
+ // Save data into Terraform state
+ data = flattenViewsFolder(createResponse.Folder)
+ resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
+}
+
+func extractViewsFolder(data *ViewsFolderModel) *cxsdk.ViewFolder {
+ return &cxsdk.ViewFolder{
+ Id: utils.TypeStringToWrapperspbString(data.Id),
+ Name: utils.TypeStringToWrapperspbString(data.Name),
+ }
+}
+
+func flattenViewsFolder(viewsFolder *cxsdk.ViewFolder) *ViewsFolderModel {
+ return &ViewsFolderModel{
+ Id: utils.WrapperspbStringToTypeString(viewsFolder.Id),
+ Name: utils.WrapperspbStringToTypeString(viewsFolder.Name),
+ }
+}
+
+func (r *ViewsFolderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data *ViewsFolderModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ id := data.Id.ValueString()
+ readReq := &cxsdk.GetViewFolderRequest{
+ Id: wrapperspb.String(id),
+ }
+ log.Printf("[INFO] Reading views-folder with ID: %s", id)
+ readResp, err := r.client.Get(ctx, readReq)
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ if cxsdk.Code(err) == codes.NotFound {
+ resp.Diagnostics.AddWarning(
+ fmt.Sprintf("Views-Folder %q is in state, but no longer exists in Coralogix backend", id),
+ fmt.Sprintf("%s will be recreated when you apply", id),
+ )
+ resp.State.RemoveResource(ctx)
+ } else {
+ resp.Diagnostics.AddError(
+ "Error reading views-folder",
+ utils.FormatRpcErrors(err, cxsdk.GetViewFolderRPC, protojson.Format(readReq)),
+ )
+ }
+ return
+ }
+ log.Printf("[INFO] Views-Folder read successfully: %s", protojson.Format(readResp.Folder))
+
+ // Flatten the response into the model
+ data = flattenViewsFolder(readResp.Folder)
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ViewsFolderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data *ViewsFolderModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Save updated data into Terraform state
+ updateReq := &cxsdk.ReplaceViewFolderRequest{
+ Folder: extractViewsFolder(data),
+ }
+
+ log.Printf("[INFO] Updating views-folder in state: %s", protojson.Format(updateReq))
+ updateResp, err := r.client.Replace(ctx, updateReq)
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ resp.Diagnostics.AddError(
+ "Error updating views-folder in state",
+ utils.FormatRpcErrors(err, cxsdk.ReplaceViewFolderRPC, protojson.Format(updateReq)),
+ )
+ return
+ }
+ log.Printf("[INFO] Views-Folder updated in state successfully: %s", protojson.Format(updateResp.Folder))
+
+ // Flatten the response into the model
+ data = flattenViewsFolder(updateResp.Folder)
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ViewsFolderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data *ViewsFolderModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ id := data.Id.ValueString()
+ _, err := r.client.Delete(ctx, &cxsdk.DeleteViewFolderRequest{Id: wrapperspb.String(id)})
+ if err != nil {
+ log.Printf("[ERROR] Received error: %s", err.Error())
+ if cxsdk.Code(err) == codes.NotFound {
+ resp.Diagnostics.AddWarning(
+ fmt.Sprintf("Views-Folder %q is in state, but no longer exists in Coralogix backend", id),
+ fmt.Sprintf("%s will be removed from state", id),
+ )
+ resp.State.RemoveResource(ctx)
+ } else {
+ resp.Diagnostics.AddError(
+ "Error deleting views-folder",
+ utils.FormatRpcErrors(err, cxsdk.DeleteViewFolderRPC, id),
+ )
+ }
+ return
+ }
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+func ViewsFolderResourceSchema(ctx context.Context) schema.Schema {
+ return schema.Schema{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ Description: "id",
+ MarkdownDescription: "id",
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "Name of the views-folder",
+ MarkdownDescription: "Name of the views-folder",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ },
+ }
+}
+
+type ViewsFolderModel struct {
+ Id types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+}
diff --git a/internal/provider/data_source_coralogix_view_test.go b/internal/provider/data_source_coralogix_view_test.go
new file mode 100644
index 000000000..509444d49
--- /dev/null
+++ b/internal/provider/data_source_coralogix_view_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccCoralogixDataSourceView_basic(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCoralogixResourceView() +
+ testAccCoralogixDataSourceView_read(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("data.coralogix_view.test", "name", "Example View"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCoralogixDataSourceView_read() string {
+ return `data "coralogix_view" "test" {
+ id = coralogix_view.test.id
+ }
+`
+}
diff --git a/internal/provider/data_source_coralogix_views_folder_test.go b/internal/provider/data_source_coralogix_views_folder_test.go
new file mode 100644
index 000000000..cac2c165c
--- /dev/null
+++ b/internal/provider/data_source_coralogix_views_folder_test.go
@@ -0,0 +1,44 @@
+// Copyright 2024 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccCoralogixDataSourceViewsFolder_basic(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCoralogixResourceViewsFolder() +
+ testAccCoralogixDataSourceViewsFolder_read(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("data.coralogix_views_folder.test", "name", "Example Views Folder"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCoralogixDataSourceViewsFolder_read() string {
+ return `data "coralogix_views_folder" "test" {
+ id = coralogix_views_folder.test.id
+ }
+`
+}
diff --git a/internal/provider/resource_coralogix_view_test.go b/internal/provider/resource_coralogix_view_test.go
new file mode 100644
index 000000000..2b196c686
--- /dev/null
+++ b/internal/provider/resource_coralogix_view_test.go
@@ -0,0 +1,122 @@
+// Copyright 2025 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "testing"
+
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset"
+
+ cxsdk "github.com/coralogix/coralogix-management-sdk/go"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+func TestAccCoralogixResourceView(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckViewDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCoralogixResourceView(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("coralogix_view.test", "id"),
+ resource.TestCheckResourceAttr("coralogix_view.test", "name", "Example View"),
+ resource.TestCheckResourceAttr("coralogix_view.test", "time_selection.custom_selection.from_time", "2023-01-01T00:00:00Z"),
+ resource.TestCheckResourceAttr("coralogix_view.test", "time_selection.custom_selection.to_time", "2023-01-02T00:00:00Z"),
+ resource.TestCheckResourceAttr("coralogix_view.test", "search_query.query", "error OR warning"),
+ ),
+ },
+ {
+ ResourceName: "coralogix_view.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ {
+ Config: testAccCoralogixResourceUpdatedView(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("coralogix_view.test", "id"),
+ resource.TestCheckResourceAttr("coralogix_view.test", "name", "Example View Updated"),
+ resource.TestCheckResourceAttr("coralogix_view.test", "time_selection.quick_selection.seconds", "86400"), // 24 hours in seconds
+ resource.TestCheckResourceAttr("coralogix_view.test", "search_query.query", "error OR warning"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCheckViewDestroy(s *terraform.State) error {
+ client := testAccProvider.Meta().(*clientset.ClientSet).Views()
+ ctx := context.TODO()
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "coralogix_view" {
+ continue
+ }
+
+ if rs.Primary.ID == "" {
+ return nil
+ }
+
+ intID, err := strconv.Atoi(rs.Primary.ID)
+ if err != nil {
+ return fmt.Errorf("invalid ID format: %s", rs.Primary.ID)
+ }
+
+ resp, err := client.Get(ctx, &cxsdk.GetViewRequest{
+ Id: wrapperspb.Int32(int32(intID)),
+ })
+ if err == nil && resp != nil && resp.View != nil {
+ return fmt.Errorf("view still exists: %v", rs.Primary.ID)
+ }
+ }
+ return nil
+}
+
+func testAccCoralogixResourceView() string {
+ return `resource "coralogix_view" "test" {
+ name = "Example View"
+ time_selection = {
+ custom_selection = {
+ from_time = "2023-01-01T00:00:00Z"
+ to_time = "2023-01-02T00:00:00Z"
+ }
+ }
+ search_query = {
+ query = "error OR warning"
+ }
+}
+ `
+}
+
+func testAccCoralogixResourceUpdatedView() string {
+ return `resource "coralogix_view" "test" {
+ name = "Example View Updated"
+ time_selection = {
+ quick_selection = {
+ seconds = 86400 # 24 hours in seconds
+ }
+ }
+ search_query = {
+ query = "error OR warning"
+ }
+}
+ `
+}
diff --git a/internal/provider/resource_coralogix_views_folder_test.go b/internal/provider/resource_coralogix_views_folder_test.go
new file mode 100644
index 000000000..7fbbe820b
--- /dev/null
+++ b/internal/provider/resource_coralogix_views_folder_test.go
@@ -0,0 +1,91 @@
+// Copyright 2025 Coralogix Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package provider
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/coralogix/terraform-provider-coralogix/coralogix/clientset"
+
+ cxsdk "github.com/coralogix/coralogix-management-sdk/go"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+func TestAccCoralogixResourceViewsFolder(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ CheckDestroy: testAccCheckViewsFolderDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCoralogixResourceViewsFolder(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("coralogix_views_folder.test", "id"),
+ resource.TestCheckResourceAttr("coralogix_views_folder.test", "name", "Example Views Folder"),
+ ),
+ },
+ {
+ ResourceName: "coralogix_views_folder.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ {
+ Config: testAccCoralogixResourceUpdatedViewsFolder(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("coralogix_views_folder.test", "id"),
+ resource.TestCheckResourceAttr("coralogix_views_folder.test", "name", "Example Views Folder Updated"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccCheckViewsFolderDestroy(s *terraform.State) error {
+ client := testAccProvider.Meta().(*clientset.ClientSet).ViewsFolders()
+ ctx := context.TODO()
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "coralogix_views_folder" {
+ continue
+ }
+
+ if rs.Primary.ID == "" {
+ return nil
+ }
+
+ resp, err := client.Get(ctx, &cxsdk.GetViewFolderRequest{Id: wrapperspb.String(rs.Primary.ID)})
+ if err == nil && resp != nil && resp.Folder != nil {
+ return fmt.Errorf("views-folder still exists: %v", rs.Primary.ID)
+ }
+ }
+ return nil
+}
+
+func testAccCoralogixResourceViewsFolder() string {
+ return `resource "coralogix_views_folder" "test" {
+ name = "Example Views Folder"
+}
+ `
+}
+
+func testAccCoralogixResourceUpdatedViewsFolder() string {
+ return `resource "coralogix_views_folder" "test" {
+ name = "Example Views Folder Updated"
+}
+ `
+}