Compare commits

..

10 Commits

Author SHA1 Message Date
Jeff McCune
64a117b0c3 (#150) Add PlatformService.GetConfig and refactor ConfigValues proto
Problem:
The use of google.protobuf.Any was making it awkward to work with the
data provided by the user.  The structure of the form data is defined by
the platform engineer, so the intent of Any was to wrap the data in a
way we can pass over the network and persist in the database.

The escaped JSON encoding was problematic and error prone to decode on
the other end.

Solution:
Define the Platform values as a two level map with string keys, but with
protobuf message fields "sections" and "fields" respectively.  Use
google.protobuf.Value from the struct package to encode the actual
value.

Result:
In TypeScript, google.protobuf.Value encodes and decodes easily to a
JSON value.  On the go side, connect correctly handles the value as
well.

No more ugly error prone escaping:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"'${platformId}'"}' $host holos.v1alpha1.PlatformService.GetConfig
{
  "sections": {
    "org": {
      "fields": {
        "contactEmail": "jeff@openinfrastructure.co",
        "displayName": "Open Infrastructure Services LLC",
        "domain": "ois.run",
        "name": "ois"
      }
    }
  }
}
```

This return value is intended to be directly usable in the CUE code, so
we may further nest the values into a platform.spec key.
2024-05-01 21:30:30 -07:00
Jeff McCune
cf006be9cf (#150) Add SystemService DropTables and SeedDatabase
Makes it easier to reset the database and give Gary and Nate access to
the same organization I'm in so they can provide feedback.
2024-05-01 14:30:13 -07:00
Jeff McCune
45ad3d8e63 (#150) Fix 500 error when config values aren't provided
AddPlatform was failing with a 500 error trying to decode a nil byte
slice when adding a platform without providing any values.
2024-05-01 11:31:25 -07:00
Jeff McCune
441c968c4f (#150) Look up user by iss sub, not email.
Also log when orgs are created.
2024-05-01 10:02:08 -07:00
Jeff McCune
99f2763fdf (#150) Store Platform Config Form and Values as JSON
This patch changes the backend to store the platform config form
definition and the config values supplied by the form as JSON in the
database.

The gRPC API does not change with this patch, but may need to depending
on how this works and how easy it is to evolve the data model and add
features.
2024-05-01 09:11:53 -07:00
Jeff McCune
1312395a11 (#150) Fix platforms page links
The links were hard to click.  This makes the links a much larger click
target following the example at https://material.angular.io/components/list/overview#navigation-lists
2024-05-01 08:51:29 -07:00
Jeff McCune
615f147bcb (#150) Add PutPlatformConfig to store the config values
This patch is a work in progress wiring up the form to put the values to
the holos server using grpc.

In an effort to simplify the platform configuration, the structure is a
two level map with the top level being configuration sections and the
second level being the fields associated with the config section.

To support multiple kinds of values and field controls, the values are
serialized to JSON for rpc over the network and for storage in the
database.  When they values are used, either by the UI or by the `holos
render` command, they're to be unmarshalled and in-lined into the
Platform Config data structure.

Pick back up ensuring the Platform rpc handler correctly encodes and
decodes the structure to the database.

Consider changing the config_form and config_values fields to JSON field
types in the database.  It will likely make working with this a lot
easier.

With this patch we're ready to wire up the holos render command to fetch
the platform configuration and create the end to end demo.

Here's essentially what the render command will fetch and lay down as a
json file for CUE:

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f2c4e-ecde-7bcb-8b89-27a99e6cc7a1"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetPlatform | jq .platform.config.values
{
  "sections": {
    "org": {
      "values": {
        "contactEmail": "\"platform@openinfrastructure.co\"",
        "displayName": "\"Open Infrastructure Services  LLC\"",
        "domain": "\"ois.run\"",
        "name": "\"ois\""
      }
    }
  }
}
```
2024-04-30 20:21:15 -07:00
Jeff McCune
d0ad3bfc69 (#150) Add Platform Detail to edit platform config
This patch adds a /platform/:id route path to a PlatformDetail
component.  The platform detail component calls the GetPlatform method
given the platform ID and renders the platform config form on the detail
tab.

The submit button is not yet wired up.

The API for adding platforms changes, allowing raw json bytes using the
RawConfig.  The raw bytes are not presented on the read path though,
calling GetPlatforms provides the platform and the config form inline in
the response.

Use the `raw_config` field instead of `config` when creating the form
data.

```
❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
  "platform": {
    "org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
    "name": "bare2",
    "raw_config": {
      "form": "$(cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
    }
  }
}
EOF
```
2024-04-30 14:02:49 -07:00
Jeff McCune
fe58a33747 (#150) Add holos.v1alpha1.PlatformService.GetForm
The GetForm method is intended for the Angular frontend to get
[FormlyFieldConfig][1] data for each section of the Platform config.

[1]: https://formly.dev/docs/api/core/#formlyfieldconfig

Steps to exercise for later testing:

Add the form definition to the database:

```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
  "platform": {
    "org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
    "name": "bare${RANDOM}",
    "config": {
      "form": "$(cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
    }
  }
}
EOF
```

Get the form definition back out:

```

❯ grpcurl -H "x-oidc-id-token: $(holos token)" -d '{"platform_id":"018f2bc1-6590-7670-958a-9f3bc02b658f"}' jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.GetForm
{
  "apiVersion": "forms.holos.run/v1alpha1",
  "kind": "PlatformForm",
  "metadata": {
    "name": "bare"
  },
  "spec": {
    "sections": [
      {
        "name": "org",
        "displayName": "Organization",
        "description": "Organization config values are used to derive more specific configuration values throughout the platform.",
        "fieldConfigs": [
          {
            "key": "name",
            "type": "input",
            "props": {
              "label": "Name",
              "placeholder": "example",
              "description": "DNS label, e.g. 'example'",
              "required": true
            }
          },
          {
            "key": "domain",
            "type": "input",
            "props": {
              "label": "Domain",
              "placeholder": "example.com",
              "description": "DNS domain, e.g. 'example.com'",
              "required": true
            }
          },
          {
            "key": "displayName",
            "type": "input",
            "props": {
              "label": "Display Name",
              "placeholder": "Example Organization",
              "description": "Display name, e.g. 'Example Organization'",
              "required": true
            }
          },
          {
            "key": "contactEmail",
            "type": "input",
            "props": {
              "label": "Contact Email",
              "placeholder": "platform-team@example.com",
              "description": "Technical contact email address",
              "required": true
            }
          }
        ]
      }
    ]
  }
}
```

References

```
❯ cue export ./forms/platform/ --out yaml | yq
apiVersion: forms.holos.run/v1alpha1
kind: PlatformForm
metadata:
  name: bare
spec:
  sections:
    - name: org
      displayName: Organization
      description: Organization config values are used to derive more specific configuration values throughout the platform.
      fieldConfigs:
        - key: name
          type: input
          props:
            label: Name
            placeholder: example
            description: DNS label, e.g. 'example'
            required: true
        - key: domain
          type: input
          props:
            label: Domain
            placeholder: example.com
            description: DNS domain, e.g. 'example.com'
            required: true
        - key: displayName
          type: input
          props:
            label: Display Name
            placeholder: Example Organization
            description: Display name, e.g. 'Example Organization'
            required: true
        - key: contactEmail
          type: input
          props:
            label: Contact Email
            placeholder: platform-team@example.com
            description: Technical contact email address
            required: true
```
2024-04-29 14:24:16 -07:00
Jeff McCune
26e537e768 (#150) Add platform config form, values, cue
This patch adds 4 fields to the Platform table:

 1. Config Form represents the JSON FormlyFieldConfig for the UI.
 2. Config CUE represents the CUE file containing a definition the
    Config Values must unify with.
 3. Config Definition is the CUE definition variable name used to unify
    the values with the cue code.  Should be #PlatformSpec in most
    cases.
 4. Config Values represents the JSON values provided by the UI.

The use case is the platform engineer defines the #PlatformSpec in cue,
and provides the form field config.  The platform engineer then provides
1-3 above when adding or updating a Platform.

The UI then presents the form to the end user and provides values for 4
when the user submits the form.

This patch also refactors the AddPlatform method to accept a Platform
message.  To do so we make the id field optional since it is server
assigned.

The patch also adds a database constraint to ensure platform names are
unique within the scope of an organization.

Results:

Note how the CUE representation of the Platform Form is exported to JSON
then converted to a base64 encoded string, which is the protobuf JSON
representation of a bytes[] value.

```
grpcurl -H "x-oidc-id-token: $(holos token)" -d @ jeff.app.dev.k2.holos.run:443 holos.v1alpha1.PlatformService.AddPlatform <<EOF
{
  "platform": {
    "id": "0d3dc0c0-bbc8-41f8-8c6e-75f0476509d6",
    "org_id": "018f27cd-e5ac-7f98-bfe1-2dbab208a48c",
    "name": "bare",
    "config": {
      "form": "$(cd internal/platforms/bare && cue export ./forms/platform/ --out json | jq -cM | base64 -w0)"
    }
  }
}
EOF
```

Note the requested platform ID is ignored.

```
{
  "platforms": [
    {
      "id": "018f2af9-f7ba-772a-9db6-f985ece8fed1",
      "timestamps": {
        "createdAt": "2024-04-29T17:49:36.058379Z",
        "updatedAt": "2024-04-29T17:49:36.058379Z"
      },
      "name": "bare",
      "creator": {
        "id": "018f27cd-e591-7f98-a9d2-416167282d37"
      },
      "config": {
        "form": "eyJhcGlWZXJzaW9uIjoiZm9ybXMuaG9sb3MucnVuL3YxYWxwaGExIiwia2luZCI6IlBsYXRmb3JtRm9ybSIsIm1ldGFkYXRhIjp7Im5hbWUiOiJiYXJlIn0sInNwZWMiOnsic2VjdGlvbnMiOlt7Im5hbWUiOiJvcmciLCJkaXNwbGF5TmFtZSI6Ik9yZ2FuaXphdGlvbiIsImRlc2NyaXB0aW9uIjoiT3JnYW5pemF0aW9uIGNvbmZpZyB2YWx1ZXMgYXJlIHVzZWQgdG8gZGVyaXZlIG1vcmUgc3BlY2lmaWMgY29uZmlndXJhdGlvbiB2YWx1ZXMgdGhyb3VnaG91dCB0aGUgcGxhdGZvcm0uIiwiZmllbGRDb25maWdzIjpbeyJrZXkiOiJuYW1lIiwidHlwZSI6ImlucHV0IiwicHJvcHMiOnsibGFiZWwiOiJOYW1lIiwicGxhY2Vob2xkZXIiOiJleGFtcGxlIiwiZGVzY3JpcHRpb24iOiJETlMgbGFiZWwsIGUuZy4gJ2V4YW1wbGUnIiwicmVxdWlyZWQiOnRydWV9fSx7ImtleSI6ImRvbWFpbiIsInR5cGUiOiJpbnB1dCIsInByb3BzIjp7ImxhYmVsIjoiRG9tYWluIiwicGxhY2Vob2xkZXIiOiJleGFtcGxlLmNvbSIsImRlc2NyaXB0aW9uIjoiRE5TIGRvbWFpbiwgZS5nLiAnZXhhbXBsZS5jb20nIiwicmVxdWlyZWQiOnRydWV9fSx7ImtleSI6ImRpc3BsYXlOYW1lIiwidHlwZSI6ImlucHV0IiwicHJvcHMiOnsibGFiZWwiOiJEaXNwbGF5IE5hbWUiLCJwbGFjZWhvbGRlciI6IkV4YW1wbGUgT3JnYW5pemF0aW9uIiwiZGVzY3JpcHRpb24iOiJEaXNwbGF5IG5hbWUsIGUuZy4gJ0V4YW1wbGUgT3JnYW5pemF0aW9uJyIsInJlcXVpcmVkIjp0cnVlfX0seyJrZXkiOiJjb250YWN0RW1haWwiLCJ0eXBlIjoiaW5wdXQiLCJwcm9wcyI6eyJsYWJlbCI6IkNvbnRhY3QgRW1haWwiLCJwbGFjZWhvbGRlciI6InBsYXRmb3JtLXRlYW1AZXhhbXBsZS5jb20iLCJkZXNjcmlwdGlvbiI6IlRlY2huaWNhbCBjb250YWN0IGVtYWlsIGFkZHJlc3MiLCJyZXF1aXJlZCI6dHJ1ZX19XX1dfX0K"
      }
    }
  ]
}
```
2024-04-29 10:53:23 -07:00
50 changed files with 2683 additions and 698 deletions

16
hack/setup/bare Executable file
View File

@@ -0,0 +1,16 @@
#! /bin/bash
set -euo pipefail
TOPLEVEL="$(cd $(dirname "$0") && git rev-parse --show-toplevel)"
host="jeff.app.dev.k2.holos.run:443"
read -p "Reset all data in $host? " choice
case "$choice" in
y|Y) echo "proceeding...";;
*) exit 1;;
esac
grpcurl -H "x-oidc-id-token: $(holos token)" $host holos.v1alpha1.SystemService.DropTables
grpcurl -H "x-oidc-id-token: $(holos token)" $host holos.v1alpha1.SystemService.SeedDatabase

View File

@@ -38,6 +38,10 @@ var (
{Name: "updated_at", Type: field.TypeTime},
{Name: "name", Type: field.TypeString},
{Name: "display_name", Type: field.TypeString},
{Name: "config_form", Type: field.TypeJSON, Nullable: true},
{Name: "config_values", Type: field.TypeJSON, Nullable: true},
{Name: "config_cue", Type: field.TypeBytes, Nullable: true},
{Name: "config_definition", Type: field.TypeString, Nullable: true},
{Name: "creator_id", Type: field.TypeUUID},
{Name: "org_id", Type: field.TypeUUID},
}
@@ -49,17 +53,24 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "platforms_users_creator",
Columns: []*schema.Column{PlatformsColumns[5]},
Columns: []*schema.Column{PlatformsColumns[9]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "platforms_organizations_organization",
Columns: []*schema.Column{PlatformsColumns[6]},
Columns: []*schema.Column{PlatformsColumns[10]},
RefColumns: []*schema.Column{OrganizationsColumns[0]},
OnDelete: schema.NoAction,
},
},
Indexes: []*schema.Index{
{
Name: "platform_org_id_name",
Unique: true,
Columns: []*schema.Column{PlatformsColumns[10], PlatformsColumns[3]},
},
},
}
// UsersColumns holds the columns for the "users" table.
UsersColumns = []*schema.Column{

View File

@@ -16,6 +16,7 @@ import (
"github.com/holos-run/holos/internal/ent/platform"
"github.com/holos-run/holos/internal/ent/predicate"
"github.com/holos-run/holos/internal/ent/user"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
)
const (
@@ -812,6 +813,10 @@ type PlatformMutation struct {
updated_at *time.Time
name *string
display_name *string
config_form **holos.PlatformForm
config_values **holos.ConfigValues
config_cue *[]byte
config_definition *string
clearedFields map[string]struct{}
creator *uuid.UUID
clearedcreator bool
@@ -1142,6 +1147,202 @@ func (m *PlatformMutation) ResetCreatorID() {
m.creator = nil
}
// SetConfigForm sets the "config_form" field.
func (m *PlatformMutation) SetConfigForm(hf *holos.PlatformForm) {
m.config_form = &hf
}
// ConfigForm returns the value of the "config_form" field in the mutation.
func (m *PlatformMutation) ConfigForm() (r *holos.PlatformForm, exists bool) {
v := m.config_form
if v == nil {
return
}
return *v, true
}
// OldConfigForm returns the old "config_form" field's value of the Platform entity.
// If the Platform object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PlatformMutation) OldConfigForm(ctx context.Context) (v *holos.PlatformForm, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldConfigForm is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldConfigForm requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldConfigForm: %w", err)
}
return oldValue.ConfigForm, nil
}
// ClearConfigForm clears the value of the "config_form" field.
func (m *PlatformMutation) ClearConfigForm() {
m.config_form = nil
m.clearedFields[platform.FieldConfigForm] = struct{}{}
}
// ConfigFormCleared returns if the "config_form" field was cleared in this mutation.
func (m *PlatformMutation) ConfigFormCleared() bool {
_, ok := m.clearedFields[platform.FieldConfigForm]
return ok
}
// ResetConfigForm resets all changes to the "config_form" field.
func (m *PlatformMutation) ResetConfigForm() {
m.config_form = nil
delete(m.clearedFields, platform.FieldConfigForm)
}
// SetConfigValues sets the "config_values" field.
func (m *PlatformMutation) SetConfigValues(hv *holos.ConfigValues) {
m.config_values = &hv
}
// ConfigValues returns the value of the "config_values" field in the mutation.
func (m *PlatformMutation) ConfigValues() (r *holos.ConfigValues, exists bool) {
v := m.config_values
if v == nil {
return
}
return *v, true
}
// OldConfigValues returns the old "config_values" field's value of the Platform entity.
// If the Platform object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PlatformMutation) OldConfigValues(ctx context.Context) (v *holos.ConfigValues, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldConfigValues is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldConfigValues requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldConfigValues: %w", err)
}
return oldValue.ConfigValues, nil
}
// ClearConfigValues clears the value of the "config_values" field.
func (m *PlatformMutation) ClearConfigValues() {
m.config_values = nil
m.clearedFields[platform.FieldConfigValues] = struct{}{}
}
// ConfigValuesCleared returns if the "config_values" field was cleared in this mutation.
func (m *PlatformMutation) ConfigValuesCleared() bool {
_, ok := m.clearedFields[platform.FieldConfigValues]
return ok
}
// ResetConfigValues resets all changes to the "config_values" field.
func (m *PlatformMutation) ResetConfigValues() {
m.config_values = nil
delete(m.clearedFields, platform.FieldConfigValues)
}
// SetConfigCue sets the "config_cue" field.
func (m *PlatformMutation) SetConfigCue(b []byte) {
m.config_cue = &b
}
// ConfigCue returns the value of the "config_cue" field in the mutation.
func (m *PlatformMutation) ConfigCue() (r []byte, exists bool) {
v := m.config_cue
if v == nil {
return
}
return *v, true
}
// OldConfigCue returns the old "config_cue" field's value of the Platform entity.
// If the Platform object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PlatformMutation) OldConfigCue(ctx context.Context) (v []byte, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldConfigCue is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldConfigCue requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldConfigCue: %w", err)
}
return oldValue.ConfigCue, nil
}
// ClearConfigCue clears the value of the "config_cue" field.
func (m *PlatformMutation) ClearConfigCue() {
m.config_cue = nil
m.clearedFields[platform.FieldConfigCue] = struct{}{}
}
// ConfigCueCleared returns if the "config_cue" field was cleared in this mutation.
func (m *PlatformMutation) ConfigCueCleared() bool {
_, ok := m.clearedFields[platform.FieldConfigCue]
return ok
}
// ResetConfigCue resets all changes to the "config_cue" field.
func (m *PlatformMutation) ResetConfigCue() {
m.config_cue = nil
delete(m.clearedFields, platform.FieldConfigCue)
}
// SetConfigDefinition sets the "config_definition" field.
func (m *PlatformMutation) SetConfigDefinition(s string) {
m.config_definition = &s
}
// ConfigDefinition returns the value of the "config_definition" field in the mutation.
func (m *PlatformMutation) ConfigDefinition() (r string, exists bool) {
v := m.config_definition
if v == nil {
return
}
return *v, true
}
// OldConfigDefinition returns the old "config_definition" field's value of the Platform entity.
// If the Platform object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *PlatformMutation) OldConfigDefinition(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldConfigDefinition is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldConfigDefinition requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldConfigDefinition: %w", err)
}
return oldValue.ConfigDefinition, nil
}
// ClearConfigDefinition clears the value of the "config_definition" field.
func (m *PlatformMutation) ClearConfigDefinition() {
m.config_definition = nil
m.clearedFields[platform.FieldConfigDefinition] = struct{}{}
}
// ConfigDefinitionCleared returns if the "config_definition" field was cleared in this mutation.
func (m *PlatformMutation) ConfigDefinitionCleared() bool {
_, ok := m.clearedFields[platform.FieldConfigDefinition]
return ok
}
// ResetConfigDefinition resets all changes to the "config_definition" field.
func (m *PlatformMutation) ResetConfigDefinition() {
m.config_definition = nil
delete(m.clearedFields, platform.FieldConfigDefinition)
}
// ClearCreator clears the "creator" edge to the User entity.
func (m *PlatformMutation) ClearCreator() {
m.clearedcreator = true
@@ -1243,7 +1444,7 @@ func (m *PlatformMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *PlatformMutation) Fields() []string {
fields := make([]string, 0, 6)
fields := make([]string, 0, 10)
if m.created_at != nil {
fields = append(fields, platform.FieldCreatedAt)
}
@@ -1262,6 +1463,18 @@ func (m *PlatformMutation) Fields() []string {
if m.creator != nil {
fields = append(fields, platform.FieldCreatorID)
}
if m.config_form != nil {
fields = append(fields, platform.FieldConfigForm)
}
if m.config_values != nil {
fields = append(fields, platform.FieldConfigValues)
}
if m.config_cue != nil {
fields = append(fields, platform.FieldConfigCue)
}
if m.config_definition != nil {
fields = append(fields, platform.FieldConfigDefinition)
}
return fields
}
@@ -1282,6 +1495,14 @@ func (m *PlatformMutation) Field(name string) (ent.Value, bool) {
return m.DisplayName()
case platform.FieldCreatorID:
return m.CreatorID()
case platform.FieldConfigForm:
return m.ConfigForm()
case platform.FieldConfigValues:
return m.ConfigValues()
case platform.FieldConfigCue:
return m.ConfigCue()
case platform.FieldConfigDefinition:
return m.ConfigDefinition()
}
return nil, false
}
@@ -1303,6 +1524,14 @@ func (m *PlatformMutation) OldField(ctx context.Context, name string) (ent.Value
return m.OldDisplayName(ctx)
case platform.FieldCreatorID:
return m.OldCreatorID(ctx)
case platform.FieldConfigForm:
return m.OldConfigForm(ctx)
case platform.FieldConfigValues:
return m.OldConfigValues(ctx)
case platform.FieldConfigCue:
return m.OldConfigCue(ctx)
case platform.FieldConfigDefinition:
return m.OldConfigDefinition(ctx)
}
return nil, fmt.Errorf("unknown Platform field %s", name)
}
@@ -1354,6 +1583,34 @@ func (m *PlatformMutation) SetField(name string, value ent.Value) error {
}
m.SetCreatorID(v)
return nil
case platform.FieldConfigForm:
v, ok := value.(*holos.PlatformForm)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetConfigForm(v)
return nil
case platform.FieldConfigValues:
v, ok := value.(*holos.ConfigValues)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetConfigValues(v)
return nil
case platform.FieldConfigCue:
v, ok := value.([]byte)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetConfigCue(v)
return nil
case platform.FieldConfigDefinition:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetConfigDefinition(v)
return nil
}
return fmt.Errorf("unknown Platform field %s", name)
}
@@ -1383,7 +1640,20 @@ func (m *PlatformMutation) AddField(name string, value ent.Value) error {
// ClearedFields returns all nullable fields that were cleared during this
// mutation.
func (m *PlatformMutation) ClearedFields() []string {
return nil
var fields []string
if m.FieldCleared(platform.FieldConfigForm) {
fields = append(fields, platform.FieldConfigForm)
}
if m.FieldCleared(platform.FieldConfigValues) {
fields = append(fields, platform.FieldConfigValues)
}
if m.FieldCleared(platform.FieldConfigCue) {
fields = append(fields, platform.FieldConfigCue)
}
if m.FieldCleared(platform.FieldConfigDefinition) {
fields = append(fields, platform.FieldConfigDefinition)
}
return fields
}
// FieldCleared returns a boolean indicating if a field with the given name was
@@ -1396,6 +1666,20 @@ func (m *PlatformMutation) FieldCleared(name string) bool {
// ClearField clears the value of the field with the given name. It returns an
// error if the field is not defined in the schema.
func (m *PlatformMutation) ClearField(name string) error {
switch name {
case platform.FieldConfigForm:
m.ClearConfigForm()
return nil
case platform.FieldConfigValues:
m.ClearConfigValues()
return nil
case platform.FieldConfigCue:
m.ClearConfigCue()
return nil
case platform.FieldConfigDefinition:
m.ClearConfigDefinition()
return nil
}
return fmt.Errorf("unknown Platform nullable field %s", name)
}
@@ -1421,6 +1705,18 @@ func (m *PlatformMutation) ResetField(name string) error {
case platform.FieldCreatorID:
m.ResetCreatorID()
return nil
case platform.FieldConfigForm:
m.ResetConfigForm()
return nil
case platform.FieldConfigValues:
m.ResetConfigValues()
return nil
case platform.FieldConfigCue:
m.ResetConfigCue()
return nil
case platform.FieldConfigDefinition:
m.ResetConfigDefinition()
return nil
}
return fmt.Errorf("unknown Platform field %s", name)
}

View File

@@ -3,6 +3,7 @@
package ent
import (
"encoding/json"
"fmt"
"strings"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/platform"
"github.com/holos-run/holos/internal/ent/user"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
)
// Platform is the model entity for the Platform schema.
@@ -32,6 +34,14 @@ type Platform struct {
DisplayName string `json:"display_name,omitempty"`
// CreatorID holds the value of the "creator_id" field.
CreatorID uuid.UUID `json:"creator_id,omitempty"`
// JSON holos.PlatformForm representing the platform data entry form.
ConfigForm *holos.PlatformForm `json:"config_form,omitempty"`
// JSON holos.ConfigValues representing the platform config values.
ConfigValues *holos.ConfigValues `json:"config_values,omitempty"`
// Opaque bytes representing the CUE definition of the config struct.
ConfigCue []byte `json:"config_cue,omitempty"`
// The definition name to vet config_values against config_cue e.g. '#PlatformSpec'
ConfigDefinition string `json:"config_definition,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the PlatformQuery when eager-loading is set.
Edges PlatformEdges `json:"edges"`
@@ -76,7 +86,9 @@ func (*Platform) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case platform.FieldName, platform.FieldDisplayName:
case platform.FieldConfigForm, platform.FieldConfigValues, platform.FieldConfigCue:
values[i] = new([]byte)
case platform.FieldName, platform.FieldDisplayName, platform.FieldConfigDefinition:
values[i] = new(sql.NullString)
case platform.FieldCreatedAt, platform.FieldUpdatedAt:
values[i] = new(sql.NullTime)
@@ -139,6 +151,34 @@ func (pl *Platform) assignValues(columns []string, values []any) error {
} else if value != nil {
pl.CreatorID = *value
}
case platform.FieldConfigForm:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field config_form", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &pl.ConfigForm); err != nil {
return fmt.Errorf("unmarshal field config_form: %w", err)
}
}
case platform.FieldConfigValues:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field config_values", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &pl.ConfigValues); err != nil {
return fmt.Errorf("unmarshal field config_values: %w", err)
}
}
case platform.FieldConfigCue:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field config_cue", values[i])
} else if value != nil {
pl.ConfigCue = *value
}
case platform.FieldConfigDefinition:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field config_definition", values[i])
} else if value.Valid {
pl.ConfigDefinition = value.String
}
default:
pl.selectValues.Set(columns[i], values[i])
}
@@ -202,6 +242,18 @@ func (pl *Platform) String() string {
builder.WriteString(", ")
builder.WriteString("creator_id=")
builder.WriteString(fmt.Sprintf("%v", pl.CreatorID))
builder.WriteString(", ")
builder.WriteString("config_form=")
builder.WriteString(fmt.Sprintf("%v", pl.ConfigForm))
builder.WriteString(", ")
builder.WriteString("config_values=")
builder.WriteString(fmt.Sprintf("%v", pl.ConfigValues))
builder.WriteString(", ")
builder.WriteString("config_cue=")
builder.WriteString(fmt.Sprintf("%v", pl.ConfigCue))
builder.WriteString(", ")
builder.WriteString("config_definition=")
builder.WriteString(pl.ConfigDefinition)
builder.WriteByte(')')
return builder.String()
}

View File

@@ -27,6 +27,14 @@ const (
FieldDisplayName = "display_name"
// FieldCreatorID holds the string denoting the creator_id field in the database.
FieldCreatorID = "creator_id"
// FieldConfigForm holds the string denoting the config_form field in the database.
FieldConfigForm = "config_form"
// FieldConfigValues holds the string denoting the config_values field in the database.
FieldConfigValues = "config_values"
// FieldConfigCue holds the string denoting the config_cue field in the database.
FieldConfigCue = "config_cue"
// FieldConfigDefinition holds the string denoting the config_definition field in the database.
FieldConfigDefinition = "config_definition"
// EdgeCreator holds the string denoting the creator edge name in mutations.
EdgeCreator = "creator"
// EdgeOrganization holds the string denoting the organization edge name in mutations.
@@ -58,6 +66,10 @@ var Columns = []string{
FieldName,
FieldDisplayName,
FieldCreatorID,
FieldConfigForm,
FieldConfigValues,
FieldConfigCue,
FieldConfigDefinition,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@@ -121,6 +133,11 @@ func ByCreatorID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatorID, opts...).ToFunc()
}
// ByConfigDefinition orders the results by the config_definition field.
func ByConfigDefinition(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldConfigDefinition, opts...).ToFunc()
}
// ByCreatorField orders the results by creator field.
func ByCreatorField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -86,6 +86,16 @@ func CreatorID(v uuid.UUID) predicate.Platform {
return predicate.Platform(sql.FieldEQ(FieldCreatorID, v))
}
// ConfigCue applies equality check predicate on the "config_cue" field. It's identical to ConfigCueEQ.
func ConfigCue(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldEQ(FieldConfigCue, v))
}
// ConfigDefinition applies equality check predicate on the "config_definition" field. It's identical to ConfigDefinitionEQ.
func ConfigDefinition(v string) predicate.Platform {
return predicate.Platform(sql.FieldEQ(FieldConfigDefinition, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Platform {
return predicate.Platform(sql.FieldEQ(FieldCreatedAt, v))
@@ -336,6 +346,151 @@ func CreatorIDNotIn(vs ...uuid.UUID) predicate.Platform {
return predicate.Platform(sql.FieldNotIn(FieldCreatorID, vs...))
}
// ConfigFormIsNil applies the IsNil predicate on the "config_form" field.
func ConfigFormIsNil() predicate.Platform {
return predicate.Platform(sql.FieldIsNull(FieldConfigForm))
}
// ConfigFormNotNil applies the NotNil predicate on the "config_form" field.
func ConfigFormNotNil() predicate.Platform {
return predicate.Platform(sql.FieldNotNull(FieldConfigForm))
}
// ConfigValuesIsNil applies the IsNil predicate on the "config_values" field.
func ConfigValuesIsNil() predicate.Platform {
return predicate.Platform(sql.FieldIsNull(FieldConfigValues))
}
// ConfigValuesNotNil applies the NotNil predicate on the "config_values" field.
func ConfigValuesNotNil() predicate.Platform {
return predicate.Platform(sql.FieldNotNull(FieldConfigValues))
}
// ConfigCueEQ applies the EQ predicate on the "config_cue" field.
func ConfigCueEQ(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldEQ(FieldConfigCue, v))
}
// ConfigCueNEQ applies the NEQ predicate on the "config_cue" field.
func ConfigCueNEQ(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldNEQ(FieldConfigCue, v))
}
// ConfigCueIn applies the In predicate on the "config_cue" field.
func ConfigCueIn(vs ...[]byte) predicate.Platform {
return predicate.Platform(sql.FieldIn(FieldConfigCue, vs...))
}
// ConfigCueNotIn applies the NotIn predicate on the "config_cue" field.
func ConfigCueNotIn(vs ...[]byte) predicate.Platform {
return predicate.Platform(sql.FieldNotIn(FieldConfigCue, vs...))
}
// ConfigCueGT applies the GT predicate on the "config_cue" field.
func ConfigCueGT(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldGT(FieldConfigCue, v))
}
// ConfigCueGTE applies the GTE predicate on the "config_cue" field.
func ConfigCueGTE(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldGTE(FieldConfigCue, v))
}
// ConfigCueLT applies the LT predicate on the "config_cue" field.
func ConfigCueLT(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldLT(FieldConfigCue, v))
}
// ConfigCueLTE applies the LTE predicate on the "config_cue" field.
func ConfigCueLTE(v []byte) predicate.Platform {
return predicate.Platform(sql.FieldLTE(FieldConfigCue, v))
}
// ConfigCueIsNil applies the IsNil predicate on the "config_cue" field.
func ConfigCueIsNil() predicate.Platform {
return predicate.Platform(sql.FieldIsNull(FieldConfigCue))
}
// ConfigCueNotNil applies the NotNil predicate on the "config_cue" field.
func ConfigCueNotNil() predicate.Platform {
return predicate.Platform(sql.FieldNotNull(FieldConfigCue))
}
// ConfigDefinitionEQ applies the EQ predicate on the "config_definition" field.
func ConfigDefinitionEQ(v string) predicate.Platform {
return predicate.Platform(sql.FieldEQ(FieldConfigDefinition, v))
}
// ConfigDefinitionNEQ applies the NEQ predicate on the "config_definition" field.
func ConfigDefinitionNEQ(v string) predicate.Platform {
return predicate.Platform(sql.FieldNEQ(FieldConfigDefinition, v))
}
// ConfigDefinitionIn applies the In predicate on the "config_definition" field.
func ConfigDefinitionIn(vs ...string) predicate.Platform {
return predicate.Platform(sql.FieldIn(FieldConfigDefinition, vs...))
}
// ConfigDefinitionNotIn applies the NotIn predicate on the "config_definition" field.
func ConfigDefinitionNotIn(vs ...string) predicate.Platform {
return predicate.Platform(sql.FieldNotIn(FieldConfigDefinition, vs...))
}
// ConfigDefinitionGT applies the GT predicate on the "config_definition" field.
func ConfigDefinitionGT(v string) predicate.Platform {
return predicate.Platform(sql.FieldGT(FieldConfigDefinition, v))
}
// ConfigDefinitionGTE applies the GTE predicate on the "config_definition" field.
func ConfigDefinitionGTE(v string) predicate.Platform {
return predicate.Platform(sql.FieldGTE(FieldConfigDefinition, v))
}
// ConfigDefinitionLT applies the LT predicate on the "config_definition" field.
func ConfigDefinitionLT(v string) predicate.Platform {
return predicate.Platform(sql.FieldLT(FieldConfigDefinition, v))
}
// ConfigDefinitionLTE applies the LTE predicate on the "config_definition" field.
func ConfigDefinitionLTE(v string) predicate.Platform {
return predicate.Platform(sql.FieldLTE(FieldConfigDefinition, v))
}
// ConfigDefinitionContains applies the Contains predicate on the "config_definition" field.
func ConfigDefinitionContains(v string) predicate.Platform {
return predicate.Platform(sql.FieldContains(FieldConfigDefinition, v))
}
// ConfigDefinitionHasPrefix applies the HasPrefix predicate on the "config_definition" field.
func ConfigDefinitionHasPrefix(v string) predicate.Platform {
return predicate.Platform(sql.FieldHasPrefix(FieldConfigDefinition, v))
}
// ConfigDefinitionHasSuffix applies the HasSuffix predicate on the "config_definition" field.
func ConfigDefinitionHasSuffix(v string) predicate.Platform {
return predicate.Platform(sql.FieldHasSuffix(FieldConfigDefinition, v))
}
// ConfigDefinitionIsNil applies the IsNil predicate on the "config_definition" field.
func ConfigDefinitionIsNil() predicate.Platform {
return predicate.Platform(sql.FieldIsNull(FieldConfigDefinition))
}
// ConfigDefinitionNotNil applies the NotNil predicate on the "config_definition" field.
func ConfigDefinitionNotNil() predicate.Platform {
return predicate.Platform(sql.FieldNotNull(FieldConfigDefinition))
}
// ConfigDefinitionEqualFold applies the EqualFold predicate on the "config_definition" field.
func ConfigDefinitionEqualFold(v string) predicate.Platform {
return predicate.Platform(sql.FieldEqualFold(FieldConfigDefinition, v))
}
// ConfigDefinitionContainsFold applies the ContainsFold predicate on the "config_definition" field.
func ConfigDefinitionContainsFold(v string) predicate.Platform {
return predicate.Platform(sql.FieldContainsFold(FieldConfigDefinition, v))
}
// HasCreator applies the HasEdge predicate on the "creator" edge.
func HasCreator() predicate.Platform {
return predicate.Platform(func(s *sql.Selector) {

View File

@@ -16,6 +16,7 @@ import (
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/platform"
"github.com/holos-run/holos/internal/ent/user"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
)
// PlatformCreate is the builder for creating a Platform entity.
@@ -78,6 +79,38 @@ func (pc *PlatformCreate) SetCreatorID(u uuid.UUID) *PlatformCreate {
return pc
}
// SetConfigForm sets the "config_form" field.
func (pc *PlatformCreate) SetConfigForm(hf *holos.PlatformForm) *PlatformCreate {
pc.mutation.SetConfigForm(hf)
return pc
}
// SetConfigValues sets the "config_values" field.
func (pc *PlatformCreate) SetConfigValues(hv *holos.ConfigValues) *PlatformCreate {
pc.mutation.SetConfigValues(hv)
return pc
}
// SetConfigCue sets the "config_cue" field.
func (pc *PlatformCreate) SetConfigCue(b []byte) *PlatformCreate {
pc.mutation.SetConfigCue(b)
return pc
}
// SetConfigDefinition sets the "config_definition" field.
func (pc *PlatformCreate) SetConfigDefinition(s string) *PlatformCreate {
pc.mutation.SetConfigDefinition(s)
return pc
}
// SetNillableConfigDefinition sets the "config_definition" field if the given value is not nil.
func (pc *PlatformCreate) SetNillableConfigDefinition(s *string) *PlatformCreate {
if s != nil {
pc.SetConfigDefinition(*s)
}
return pc
}
// SetID sets the "id" field.
func (pc *PlatformCreate) SetID(u uuid.UUID) *PlatformCreate {
pc.mutation.SetID(u)
@@ -240,6 +273,22 @@ func (pc *PlatformCreate) createSpec() (*Platform, *sqlgraph.CreateSpec) {
_spec.SetField(platform.FieldDisplayName, field.TypeString, value)
_node.DisplayName = value
}
if value, ok := pc.mutation.ConfigForm(); ok {
_spec.SetField(platform.FieldConfigForm, field.TypeJSON, value)
_node.ConfigForm = value
}
if value, ok := pc.mutation.ConfigValues(); ok {
_spec.SetField(platform.FieldConfigValues, field.TypeJSON, value)
_node.ConfigValues = value
}
if value, ok := pc.mutation.ConfigCue(); ok {
_spec.SetField(platform.FieldConfigCue, field.TypeBytes, value)
_node.ConfigCue = value
}
if value, ok := pc.mutation.ConfigDefinition(); ok {
_spec.SetField(platform.FieldConfigDefinition, field.TypeString, value)
_node.ConfigDefinition = value
}
if nodes := pc.mutation.CreatorIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -386,6 +435,78 @@ func (u *PlatformUpsert) UpdateCreatorID() *PlatformUpsert {
return u
}
// SetConfigForm sets the "config_form" field.
func (u *PlatformUpsert) SetConfigForm(v *holos.PlatformForm) *PlatformUpsert {
u.Set(platform.FieldConfigForm, v)
return u
}
// UpdateConfigForm sets the "config_form" field to the value that was provided on create.
func (u *PlatformUpsert) UpdateConfigForm() *PlatformUpsert {
u.SetExcluded(platform.FieldConfigForm)
return u
}
// ClearConfigForm clears the value of the "config_form" field.
func (u *PlatformUpsert) ClearConfigForm() *PlatformUpsert {
u.SetNull(platform.FieldConfigForm)
return u
}
// SetConfigValues sets the "config_values" field.
func (u *PlatformUpsert) SetConfigValues(v *holos.ConfigValues) *PlatformUpsert {
u.Set(platform.FieldConfigValues, v)
return u
}
// UpdateConfigValues sets the "config_values" field to the value that was provided on create.
func (u *PlatformUpsert) UpdateConfigValues() *PlatformUpsert {
u.SetExcluded(platform.FieldConfigValues)
return u
}
// ClearConfigValues clears the value of the "config_values" field.
func (u *PlatformUpsert) ClearConfigValues() *PlatformUpsert {
u.SetNull(platform.FieldConfigValues)
return u
}
// SetConfigCue sets the "config_cue" field.
func (u *PlatformUpsert) SetConfigCue(v []byte) *PlatformUpsert {
u.Set(platform.FieldConfigCue, v)
return u
}
// UpdateConfigCue sets the "config_cue" field to the value that was provided on create.
func (u *PlatformUpsert) UpdateConfigCue() *PlatformUpsert {
u.SetExcluded(platform.FieldConfigCue)
return u
}
// ClearConfigCue clears the value of the "config_cue" field.
func (u *PlatformUpsert) ClearConfigCue() *PlatformUpsert {
u.SetNull(platform.FieldConfigCue)
return u
}
// SetConfigDefinition sets the "config_definition" field.
func (u *PlatformUpsert) SetConfigDefinition(v string) *PlatformUpsert {
u.Set(platform.FieldConfigDefinition, v)
return u
}
// UpdateConfigDefinition sets the "config_definition" field to the value that was provided on create.
func (u *PlatformUpsert) UpdateConfigDefinition() *PlatformUpsert {
u.SetExcluded(platform.FieldConfigDefinition)
return u
}
// ClearConfigDefinition clears the value of the "config_definition" field.
func (u *PlatformUpsert) ClearConfigDefinition() *PlatformUpsert {
u.SetNull(platform.FieldConfigDefinition)
return u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field.
// Using this option is equivalent to using:
//
@@ -507,6 +628,90 @@ func (u *PlatformUpsertOne) UpdateCreatorID() *PlatformUpsertOne {
})
}
// SetConfigForm sets the "config_form" field.
func (u *PlatformUpsertOne) SetConfigForm(v *holos.PlatformForm) *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigForm(v)
})
}
// UpdateConfigForm sets the "config_form" field to the value that was provided on create.
func (u *PlatformUpsertOne) UpdateConfigForm() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigForm()
})
}
// ClearConfigForm clears the value of the "config_form" field.
func (u *PlatformUpsertOne) ClearConfigForm() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigForm()
})
}
// SetConfigValues sets the "config_values" field.
func (u *PlatformUpsertOne) SetConfigValues(v *holos.ConfigValues) *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigValues(v)
})
}
// UpdateConfigValues sets the "config_values" field to the value that was provided on create.
func (u *PlatformUpsertOne) UpdateConfigValues() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigValues()
})
}
// ClearConfigValues clears the value of the "config_values" field.
func (u *PlatformUpsertOne) ClearConfigValues() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigValues()
})
}
// SetConfigCue sets the "config_cue" field.
func (u *PlatformUpsertOne) SetConfigCue(v []byte) *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigCue(v)
})
}
// UpdateConfigCue sets the "config_cue" field to the value that was provided on create.
func (u *PlatformUpsertOne) UpdateConfigCue() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigCue()
})
}
// ClearConfigCue clears the value of the "config_cue" field.
func (u *PlatformUpsertOne) ClearConfigCue() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigCue()
})
}
// SetConfigDefinition sets the "config_definition" field.
func (u *PlatformUpsertOne) SetConfigDefinition(v string) *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigDefinition(v)
})
}
// UpdateConfigDefinition sets the "config_definition" field to the value that was provided on create.
func (u *PlatformUpsertOne) UpdateConfigDefinition() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigDefinition()
})
}
// ClearConfigDefinition clears the value of the "config_definition" field.
func (u *PlatformUpsertOne) ClearConfigDefinition() *PlatformUpsertOne {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigDefinition()
})
}
// Exec executes the query.
func (u *PlatformUpsertOne) Exec(ctx context.Context) error {
if len(u.create.conflict) == 0 {
@@ -795,6 +1000,90 @@ func (u *PlatformUpsertBulk) UpdateCreatorID() *PlatformUpsertBulk {
})
}
// SetConfigForm sets the "config_form" field.
func (u *PlatformUpsertBulk) SetConfigForm(v *holos.PlatformForm) *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigForm(v)
})
}
// UpdateConfigForm sets the "config_form" field to the value that was provided on create.
func (u *PlatformUpsertBulk) UpdateConfigForm() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigForm()
})
}
// ClearConfigForm clears the value of the "config_form" field.
func (u *PlatformUpsertBulk) ClearConfigForm() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigForm()
})
}
// SetConfigValues sets the "config_values" field.
func (u *PlatformUpsertBulk) SetConfigValues(v *holos.ConfigValues) *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigValues(v)
})
}
// UpdateConfigValues sets the "config_values" field to the value that was provided on create.
func (u *PlatformUpsertBulk) UpdateConfigValues() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigValues()
})
}
// ClearConfigValues clears the value of the "config_values" field.
func (u *PlatformUpsertBulk) ClearConfigValues() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigValues()
})
}
// SetConfigCue sets the "config_cue" field.
func (u *PlatformUpsertBulk) SetConfigCue(v []byte) *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigCue(v)
})
}
// UpdateConfigCue sets the "config_cue" field to the value that was provided on create.
func (u *PlatformUpsertBulk) UpdateConfigCue() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigCue()
})
}
// ClearConfigCue clears the value of the "config_cue" field.
func (u *PlatformUpsertBulk) ClearConfigCue() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigCue()
})
}
// SetConfigDefinition sets the "config_definition" field.
func (u *PlatformUpsertBulk) SetConfigDefinition(v string) *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.SetConfigDefinition(v)
})
}
// UpdateConfigDefinition sets the "config_definition" field to the value that was provided on create.
func (u *PlatformUpsertBulk) UpdateConfigDefinition() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.UpdateConfigDefinition()
})
}
// ClearConfigDefinition clears the value of the "config_definition" field.
func (u *PlatformUpsertBulk) ClearConfigDefinition() *PlatformUpsertBulk {
return u.Update(func(s *PlatformUpsert) {
s.ClearConfigDefinition()
})
}
// Exec executes the query.
func (u *PlatformUpsertBulk) Exec(ctx context.Context) error {
if u.create.err != nil {

View File

@@ -16,6 +16,7 @@ import (
"github.com/holos-run/holos/internal/ent/platform"
"github.com/holos-run/holos/internal/ent/predicate"
"github.com/holos-run/holos/internal/ent/user"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
)
// PlatformUpdate is the builder for updating Platform entities.
@@ -93,6 +94,62 @@ func (pu *PlatformUpdate) SetNillableCreatorID(u *uuid.UUID) *PlatformUpdate {
return pu
}
// SetConfigForm sets the "config_form" field.
func (pu *PlatformUpdate) SetConfigForm(hf *holos.PlatformForm) *PlatformUpdate {
pu.mutation.SetConfigForm(hf)
return pu
}
// ClearConfigForm clears the value of the "config_form" field.
func (pu *PlatformUpdate) ClearConfigForm() *PlatformUpdate {
pu.mutation.ClearConfigForm()
return pu
}
// SetConfigValues sets the "config_values" field.
func (pu *PlatformUpdate) SetConfigValues(hv *holos.ConfigValues) *PlatformUpdate {
pu.mutation.SetConfigValues(hv)
return pu
}
// ClearConfigValues clears the value of the "config_values" field.
func (pu *PlatformUpdate) ClearConfigValues() *PlatformUpdate {
pu.mutation.ClearConfigValues()
return pu
}
// SetConfigCue sets the "config_cue" field.
func (pu *PlatformUpdate) SetConfigCue(b []byte) *PlatformUpdate {
pu.mutation.SetConfigCue(b)
return pu
}
// ClearConfigCue clears the value of the "config_cue" field.
func (pu *PlatformUpdate) ClearConfigCue() *PlatformUpdate {
pu.mutation.ClearConfigCue()
return pu
}
// SetConfigDefinition sets the "config_definition" field.
func (pu *PlatformUpdate) SetConfigDefinition(s string) *PlatformUpdate {
pu.mutation.SetConfigDefinition(s)
return pu
}
// SetNillableConfigDefinition sets the "config_definition" field if the given value is not nil.
func (pu *PlatformUpdate) SetNillableConfigDefinition(s *string) *PlatformUpdate {
if s != nil {
pu.SetConfigDefinition(*s)
}
return pu
}
// ClearConfigDefinition clears the value of the "config_definition" field.
func (pu *PlatformUpdate) ClearConfigDefinition() *PlatformUpdate {
pu.mutation.ClearConfigDefinition()
return pu
}
// SetCreator sets the "creator" edge to the User entity.
func (pu *PlatformUpdate) SetCreator(u *User) *PlatformUpdate {
return pu.SetCreatorID(u.ID)
@@ -199,6 +256,30 @@ func (pu *PlatformUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := pu.mutation.DisplayName(); ok {
_spec.SetField(platform.FieldDisplayName, field.TypeString, value)
}
if value, ok := pu.mutation.ConfigForm(); ok {
_spec.SetField(platform.FieldConfigForm, field.TypeJSON, value)
}
if pu.mutation.ConfigFormCleared() {
_spec.ClearField(platform.FieldConfigForm, field.TypeJSON)
}
if value, ok := pu.mutation.ConfigValues(); ok {
_spec.SetField(platform.FieldConfigValues, field.TypeJSON, value)
}
if pu.mutation.ConfigValuesCleared() {
_spec.ClearField(platform.FieldConfigValues, field.TypeJSON)
}
if value, ok := pu.mutation.ConfigCue(); ok {
_spec.SetField(platform.FieldConfigCue, field.TypeBytes, value)
}
if pu.mutation.ConfigCueCleared() {
_spec.ClearField(platform.FieldConfigCue, field.TypeBytes)
}
if value, ok := pu.mutation.ConfigDefinition(); ok {
_spec.SetField(platform.FieldConfigDefinition, field.TypeString, value)
}
if pu.mutation.ConfigDefinitionCleared() {
_spec.ClearField(platform.FieldConfigDefinition, field.TypeString)
}
if pu.mutation.CreatorCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
@@ -339,6 +420,62 @@ func (puo *PlatformUpdateOne) SetNillableCreatorID(u *uuid.UUID) *PlatformUpdate
return puo
}
// SetConfigForm sets the "config_form" field.
func (puo *PlatformUpdateOne) SetConfigForm(hf *holos.PlatformForm) *PlatformUpdateOne {
puo.mutation.SetConfigForm(hf)
return puo
}
// ClearConfigForm clears the value of the "config_form" field.
func (puo *PlatformUpdateOne) ClearConfigForm() *PlatformUpdateOne {
puo.mutation.ClearConfigForm()
return puo
}
// SetConfigValues sets the "config_values" field.
func (puo *PlatformUpdateOne) SetConfigValues(hv *holos.ConfigValues) *PlatformUpdateOne {
puo.mutation.SetConfigValues(hv)
return puo
}
// ClearConfigValues clears the value of the "config_values" field.
func (puo *PlatformUpdateOne) ClearConfigValues() *PlatformUpdateOne {
puo.mutation.ClearConfigValues()
return puo
}
// SetConfigCue sets the "config_cue" field.
func (puo *PlatformUpdateOne) SetConfigCue(b []byte) *PlatformUpdateOne {
puo.mutation.SetConfigCue(b)
return puo
}
// ClearConfigCue clears the value of the "config_cue" field.
func (puo *PlatformUpdateOne) ClearConfigCue() *PlatformUpdateOne {
puo.mutation.ClearConfigCue()
return puo
}
// SetConfigDefinition sets the "config_definition" field.
func (puo *PlatformUpdateOne) SetConfigDefinition(s string) *PlatformUpdateOne {
puo.mutation.SetConfigDefinition(s)
return puo
}
// SetNillableConfigDefinition sets the "config_definition" field if the given value is not nil.
func (puo *PlatformUpdateOne) SetNillableConfigDefinition(s *string) *PlatformUpdateOne {
if s != nil {
puo.SetConfigDefinition(*s)
}
return puo
}
// ClearConfigDefinition clears the value of the "config_definition" field.
func (puo *PlatformUpdateOne) ClearConfigDefinition() *PlatformUpdateOne {
puo.mutation.ClearConfigDefinition()
return puo
}
// SetCreator sets the "creator" edge to the User entity.
func (puo *PlatformUpdateOne) SetCreator(u *User) *PlatformUpdateOne {
return puo.SetCreatorID(u.ID)
@@ -475,6 +612,30 @@ func (puo *PlatformUpdateOne) sqlSave(ctx context.Context) (_node *Platform, err
if value, ok := puo.mutation.DisplayName(); ok {
_spec.SetField(platform.FieldDisplayName, field.TypeString, value)
}
if value, ok := puo.mutation.ConfigForm(); ok {
_spec.SetField(platform.FieldConfigForm, field.TypeJSON, value)
}
if puo.mutation.ConfigFormCleared() {
_spec.ClearField(platform.FieldConfigForm, field.TypeJSON)
}
if value, ok := puo.mutation.ConfigValues(); ok {
_spec.SetField(platform.FieldConfigValues, field.TypeJSON, value)
}
if puo.mutation.ConfigValuesCleared() {
_spec.ClearField(platform.FieldConfigValues, field.TypeJSON)
}
if value, ok := puo.mutation.ConfigCue(); ok {
_spec.SetField(platform.FieldConfigCue, field.TypeBytes, value)
}
if puo.mutation.ConfigCueCleared() {
_spec.ClearField(platform.FieldConfigCue, field.TypeBytes)
}
if value, ok := puo.mutation.ConfigDefinition(); ok {
_spec.SetField(platform.FieldConfigDefinition, field.TypeString, value)
}
if puo.mutation.ConfigDefinitionCleared() {
_spec.ClearField(platform.FieldConfigDefinition, field.TypeString)
}
if puo.mutation.CreatorCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,

View File

@@ -4,7 +4,9 @@ import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"github.com/gofrs/uuid"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
)
type Platform struct {
@@ -24,6 +26,18 @@ func (Platform) Fields() []ent.Field {
field.String("name").NotEmpty(),
field.String("display_name"),
field.UUID("creator_id", uuid.UUID{}),
field.JSON("config_form", &holos.PlatformForm{}).
Optional().
Comment("JSON holos.PlatformForm representing the platform data entry form."),
field.JSON("config_values", &holos.ConfigValues{}).
Optional().
Comment("JSON holos.ConfigValues representing the platform config values."),
field.Bytes("config_cue").
Optional().
Comment("Opaque bytes representing the CUE definition of the config struct."),
field.String("config_definition").
Optional().
Comment("The definition name to vet config_values against config_cue e.g. '#PlatformSpec'"),
}
}
@@ -39,3 +53,10 @@ func (Platform) Edges() []ent.Edge {
Required(),
}
}
func (Platform) Indexes() []ent.Index {
return []ent.Index{
// One org cannot have two platforms with the same name.
index.Fields("org_id", "name").Unique(),
}
}

View File

@@ -45,7 +45,7 @@
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",

View File

@@ -1,5 +1,5 @@
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { FormlyModule } from '@ngx-formly/core';
// import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
@@ -8,14 +8,16 @@ import { ConnectModule } from '../connect/connect.module';
import { provideClient } from "../connect/client.provider";
import { UserService } from './gen/holos/v1alpha1/user_connect';
import { OrganizationService } from './gen/holos/v1alpha1/organization_connect';
import { PlatformService } from './gen/holos/v1alpha1/platform_connect';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideRouter(routes, withComponentInputBinding()),
provideAnimationsAsync(),
// provideHttpClient(withFetch()),
provideClient(UserService),
provideClient(OrganizationService),
provideClient(PlatformService),
importProvidersFrom(
ConnectModule.forRoot({
baseUrl: window.location.origin

View File

@@ -1,15 +1,13 @@
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ClusterListComponent } from './cluster-list/cluster-list.component';
import { ErrorNotFoundComponent } from './error-not-found/error-not-found.component';
import { PlatformConfigComponent } from './views/platform-config/platform-config.component';
import { AddressFormComponent } from './examples/address-form/address-form.component';
import { PlatformsComponent } from './views/platforms/platforms.component'
import { PlatformDetailComponent } from './views/platform-detail/platform-detail.component';
export const routes: Routes = [
{ path: 'platform-config', component: PlatformConfigComponent },
{ path: 'address-form', component: AddressFormComponent },
{ path: 'platform/:id', component: PlatformDetailComponent },
{ path: 'platforms', component: PlatformsComponent },
{ path: 'home', component: HomeComponent },
{ path: 'clusters', component: ClusterListComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '', redirectTo: '/platforms', pathMatch: 'full' },
{ path: '**', component: ErrorNotFoundComponent },
];

View File

@@ -1,26 +0,0 @@
<div class="grid-container">
<h1 class="mat-h1">Clusters</h1>
<mat-grid-list cols="2" rowHeight="350px">
@for (card of cards | async; track card) {
<mat-grid-tile [colspan]="card.cols" [rowspan]="card.rows">
<mat-card class="dashboard-card">
<mat-card-header>
<mat-card-title>
{{card.title}}
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
<button mat-menu-item>Expand</button>
<button mat-menu-item>Remove</button>
</mat-menu>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content">
<div>Card Content Here</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
}
</mat-grid-list>
</div>

View File

@@ -1,21 +0,0 @@
.grid-container {
margin: 20px;
}
.dashboard-card {
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
}
.more-button {
position: absolute;
top: 5px;
right: 10px;
}
.dashboard-card-content {
text-align: center;
}

View File

@@ -1,25 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClusterListComponent } from './cluster-list.component';
describe('ClusterListComponent', () => {
let component: ClusterListComponent;
let fixture: ComponentFixture<ClusterListComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClusterListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,48 +0,0 @@
import { Component, inject } from '@angular/core';
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { map } from 'rxjs/operators';
import { AsyncPipe } from '@angular/common';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-cluster-list',
templateUrl: './cluster-list.component.html',
styleUrl: './cluster-list.component.scss',
standalone: true,
imports: [
AsyncPipe,
MatGridListModule,
MatMenuModule,
MatIconModule,
MatButtonModule,
MatCardModule
]
})
export class ClusterListComponent {
private breakpointObserver = inject(BreakpointObserver);
/** Based on the screen size, switch from standard to one column per row */
cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
map(({ matches }) => {
if (matches) {
return [
{ title: 'Card 1', cols: 1, rows: 1 },
{ title: 'Card 2', cols: 1, rows: 1 },
{ title: 'Card 3', cols: 1, rows: 1 },
{ title: 'Card 4', cols: 1, rows: 1 }
];
}
return [
{ title: 'Card 1', cols: 2, rows: 1 },
{ title: 'Card 2', cols: 1, rows: 1 },
{ title: 'Card 3', cols: 1, rows: 2 },
{ title: 'Card 4', cols: 1, rows: 1 }
];
})
);
}

View File

@@ -1,99 +0,0 @@
<form [formGroup]="addressForm" novalidate (ngSubmit)="onSubmit()">
<mat-card class="shipping-card">
<mat-card-header>
<mat-card-title>Shipping Information</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="row">
<div class="col">
<mat-form-field class="full-width">
<input matInput placeholder="Company" formControlName="company">
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col">
<mat-form-field class="full-width">
<input matInput placeholder="First name" formControlName="firstName">
@if (addressForm.controls['firstName'].hasError('required')) {
<mat-error>First name is <strong>required</strong></mat-error>
}
</mat-form-field>
</div>
<div class="col">
<mat-form-field class="full-width">
<input matInput placeholder="Last name" formControlName="lastName">
@if (addressForm.controls['lastName'].hasError('required')) {
<mat-error>Last name is <strong>required</strong></mat-error>
}
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col">
<mat-form-field class="full-width">
<textarea matInput placeholder="Address" formControlName="address"></textarea>
@if (addressForm.controls['address'].hasError('required')) {
<mat-error>Address is <strong>required</strong></mat-error>
}
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col">
@if (hasUnitNumber) {
<mat-form-field class="full-width">
<textarea matInput placeholder="Address 2" formControlName="address2"></textarea>
</mat-form-field>
} @else {
<button mat-button type="button" (click)="hasUnitNumber = !hasUnitNumber">
+ Add C/O, Apt, Suite, Unit
</button>
}
</div>
</div>
<div class="row">
<div class="col">
<mat-form-field class="full-width">
<input matInput placeholder="City" formControlName="city">
@if (addressForm.controls['city'].hasError('required')) {
<mat-error>City is <strong>required</strong></mat-error>
}
</mat-form-field>
</div>
<div class="col">
<mat-form-field class="full-width">
<mat-select placeholder="State" formControlName="state">
@for (state of states; track state) {
<mat-option [value]="state.abbreviation">{{ state.name }}</mat-option>
}
</mat-select>
@if (addressForm.controls['state'].hasError('required')) {
<mat-error>State is <strong>required</strong></mat-error>
}
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col">
<mat-form-field class="full-width">
<input matInput #postalCode maxlength="5" placeholder="Postal Code" type="number" formControlName="postalCode">
<mat-hint align="end">{{postalCode.value.length}} / 5</mat-hint>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col">
<mat-radio-group formControlName="shipping">
<mat-radio-button value="free">Free Shipping</mat-radio-button>
<mat-radio-button value="priority">Priority Shipping</mat-radio-button>
<mat-radio-button value="nextday">Next Day Shipping</mat-radio-button>
</mat-radio-group>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" type="submit">Submit</button>
</mat-card-actions>
</mat-card>
</form>

View File

@@ -1,27 +0,0 @@
.full-width {
width: 100%;
}
.shipping-card {
min-width: 120px;
margin: 20px auto;
}
.mat-radio-button {
display: block;
margin: 5px 0;
}
.row {
display: flex;
flex-direction: row;
}
.col {
flex: 1;
margin-right: 20px;
}
.col:last-child {
margin-right: 0;
}

View File

@@ -1,25 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AddressFormComponent } from './address-form.component';
describe('AddressFormComponent', () => {
let component: AddressFormComponent;
let fixture: ComponentFixture<AddressFormComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddressFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,108 +0,0 @@
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatRadioModule } from '@angular/material/radio';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrl: './address-form.component.scss',
standalone: true,
imports: [
MatInputModule,
MatButtonModule,
MatSelectModule,
MatRadioModule,
MatCardModule,
ReactiveFormsModule
]
})
export class AddressFormComponent {
private fb = inject(FormBuilder);
addressForm = this.fb.group({
company: null,
firstName: [null, Validators.required],
lastName: [null, Validators.required],
address: [null, Validators.required],
address2: null,
city: [null, Validators.required],
state: [null, Validators.required],
postalCode: [null, Validators.compose([
Validators.required, Validators.minLength(5), Validators.maxLength(5)])
],
shipping: ['free', Validators.required]
});
hasUnitNumber = false;
states = [
{name: 'Alabama', abbreviation: 'AL'},
{name: 'Alaska', abbreviation: 'AK'},
{name: 'American Samoa', abbreviation: 'AS'},
{name: 'Arizona', abbreviation: 'AZ'},
{name: 'Arkansas', abbreviation: 'AR'},
{name: 'California', abbreviation: 'CA'},
{name: 'Colorado', abbreviation: 'CO'},
{name: 'Connecticut', abbreviation: 'CT'},
{name: 'Delaware', abbreviation: 'DE'},
{name: 'District Of Columbia', abbreviation: 'DC'},
{name: 'Federated States Of Micronesia', abbreviation: 'FM'},
{name: 'Florida', abbreviation: 'FL'},
{name: 'Georgia', abbreviation: 'GA'},
{name: 'Guam', abbreviation: 'GU'},
{name: 'Hawaii', abbreviation: 'HI'},
{name: 'Idaho', abbreviation: 'ID'},
{name: 'Illinois', abbreviation: 'IL'},
{name: 'Indiana', abbreviation: 'IN'},
{name: 'Iowa', abbreviation: 'IA'},
{name: 'Kansas', abbreviation: 'KS'},
{name: 'Kentucky', abbreviation: 'KY'},
{name: 'Louisiana', abbreviation: 'LA'},
{name: 'Maine', abbreviation: 'ME'},
{name: 'Marshall Islands', abbreviation: 'MH'},
{name: 'Maryland', abbreviation: 'MD'},
{name: 'Massachusetts', abbreviation: 'MA'},
{name: 'Michigan', abbreviation: 'MI'},
{name: 'Minnesota', abbreviation: 'MN'},
{name: 'Mississippi', abbreviation: 'MS'},
{name: 'Missouri', abbreviation: 'MO'},
{name: 'Montana', abbreviation: 'MT'},
{name: 'Nebraska', abbreviation: 'NE'},
{name: 'Nevada', abbreviation: 'NV'},
{name: 'New Hampshire', abbreviation: 'NH'},
{name: 'New Jersey', abbreviation: 'NJ'},
{name: 'New Mexico', abbreviation: 'NM'},
{name: 'New York', abbreviation: 'NY'},
{name: 'North Carolina', abbreviation: 'NC'},
{name: 'North Dakota', abbreviation: 'ND'},
{name: 'Northern Mariana Islands', abbreviation: 'MP'},
{name: 'Ohio', abbreviation: 'OH'},
{name: 'Oklahoma', abbreviation: 'OK'},
{name: 'Oregon', abbreviation: 'OR'},
{name: 'Palau', abbreviation: 'PW'},
{name: 'Pennsylvania', abbreviation: 'PA'},
{name: 'Puerto Rico', abbreviation: 'PR'},
{name: 'Rhode Island', abbreviation: 'RI'},
{name: 'South Carolina', abbreviation: 'SC'},
{name: 'South Dakota', abbreviation: 'SD'},
{name: 'Tennessee', abbreviation: 'TN'},
{name: 'Texas', abbreviation: 'TX'},
{name: 'Utah', abbreviation: 'UT'},
{name: 'Vermont', abbreviation: 'VT'},
{name: 'Virgin Islands', abbreviation: 'VI'},
{name: 'Virginia', abbreviation: 'VA'},
{name: 'Washington', abbreviation: 'WA'},
{name: 'West Virginia', abbreviation: 'WV'},
{name: 'Wisconsin', abbreviation: 'WI'},
{name: 'Wyoming', abbreviation: 'WY'}
];
onSubmit(): void {
alert('Thanks!');
}
}

View File

@@ -4,7 +4,21 @@
// @ts-nocheck
import { MethodKind } from "@bufbuild/protobuf";
import { AddPlatformRequest, GetPlatformsRequest, GetPlatformsResponse } from "./platform_pb.js";
import { AddPlatformRequest, ConfigValues, GetPlatformConfigRequest, GetPlatformRequest, GetPlatformResponse, GetPlatformsRequest, GetPlatformsResponse, PutPlatformConfigRequest } from "./platform_pb.js";
/**
* @generated from rpc holos.v1alpha1.PlatformService.AddPlatform
*/
export const addPlatform = {
localName: "addPlatform",
name: "AddPlatform",
kind: MethodKind.Unary,
I: AddPlatformRequest,
O: GetPlatformsResponse,
service: {
typeName: "holos.v1alpha1.PlatformService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.PlatformService.GetPlatforms
@@ -21,14 +35,44 @@ export const getPlatforms = {
} as const;
/**
* @generated from rpc holos.v1alpha1.PlatformService.AddPlatform
* @generated from rpc holos.v1alpha1.PlatformService.GetPlatform
*/
export const addPlatform = {
localName: "addPlatform",
name: "AddPlatform",
export const getPlatform = {
localName: "getPlatform",
name: "GetPlatform",
kind: MethodKind.Unary,
I: AddPlatformRequest,
O: GetPlatformsResponse,
I: GetPlatformRequest,
O: GetPlatformResponse,
service: {
typeName: "holos.v1alpha1.PlatformService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.PlatformService.PutPlatformConfig
*/
export const putPlatformConfig = {
localName: "putPlatformConfig",
name: "PutPlatformConfig",
kind: MethodKind.Unary,
I: PutPlatformConfigRequest,
O: GetPlatformResponse,
service: {
typeName: "holos.v1alpha1.PlatformService"
}
} as const;
/**
* GetConfig provides the unmarshalled config values for use with CUE
*
* @generated from rpc holos.v1alpha1.PlatformService.GetConfig
*/
export const getConfig = {
localName: "getConfig",
name: "GetConfig",
kind: MethodKind.Unary,
I: GetPlatformConfigRequest,
O: ConfigValues,
service: {
typeName: "holos.v1alpha1.PlatformService"
}

View File

@@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import { AddPlatformRequest, GetPlatformsRequest, GetPlatformsResponse } from "./platform_pb.js";
import { AddPlatformRequest, ConfigValues, GetPlatformConfigRequest, GetPlatformRequest, GetPlatformResponse, GetPlatformsRequest, GetPlatformsResponse, PutPlatformConfigRequest } from "./platform_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
@@ -12,6 +12,15 @@ import { MethodKind } from "@bufbuild/protobuf";
export const PlatformService = {
typeName: "holos.v1alpha1.PlatformService",
methods: {
/**
* @generated from rpc holos.v1alpha1.PlatformService.AddPlatform
*/
addPlatform: {
name: "AddPlatform",
I: AddPlatformRequest,
O: GetPlatformsResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.PlatformService.GetPlatforms
*/
@@ -22,12 +31,32 @@ export const PlatformService = {
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.PlatformService.AddPlatform
* @generated from rpc holos.v1alpha1.PlatformService.GetPlatform
*/
addPlatform: {
name: "AddPlatform",
I: AddPlatformRequest,
O: GetPlatformsResponse,
getPlatform: {
name: "GetPlatform",
I: GetPlatformRequest,
O: GetPlatformResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.PlatformService.PutPlatformConfig
*/
putPlatformConfig: {
name: "PutPlatformConfig",
I: PutPlatformConfigRequest,
O: GetPlatformResponse,
kind: MethodKind.Unary,
},
/**
* GetConfig provides the unmarshalled config values for use with CUE
*
* @generated from rpc holos.v1alpha1.PlatformService.GetConfig
*/
getConfig: {
name: "GetConfig",
I: GetPlatformConfigRequest,
O: ConfigValues,
kind: MethodKind.Unary,
},
}

View File

@@ -4,10 +4,188 @@
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
import { Message, proto3, Value } from "@bufbuild/protobuf";
import { Timestamps } from "./timestamps_pb.js";
import { Creator } from "./user_pb.js";
/**
* RawConfig represents the raw form configuration as opaque bytes. Used for input.
*
* @generated from message holos.v1alpha1.RawConfig
*/
export class RawConfig extends Message<RawConfig> {
/**
* @generated from field: bytes form = 1;
*/
form = new Uint8Array(0);
/**
* @generated from field: bytes values = 2;
*/
values = new Uint8Array(0);
/**
* @generated from field: bytes cue = 3;
*/
cue = new Uint8Array(0);
/**
* @generated from field: string definition = 4;
*/
definition = "";
constructor(data?: PartialMessage<RawConfig>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.RawConfig";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "form", kind: "scalar", T: 12 /* ScalarType.BYTES */ },
{ no: 2, name: "values", kind: "scalar", T: 12 /* ScalarType.BYTES */ },
{ no: 3, name: "cue", kind: "scalar", T: 12 /* ScalarType.BYTES */ },
{ no: 4, name: "definition", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RawConfig {
return new RawConfig().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RawConfig {
return new RawConfig().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RawConfig {
return new RawConfig().fromJsonString(jsonString, options);
}
static equals(a: RawConfig | PlainMessage<RawConfig> | undefined, b: RawConfig | PlainMessage<RawConfig> | undefined): boolean {
return proto3.util.equals(RawConfig, a, b);
}
}
/**
* @generated from message holos.v1alpha1.Config
*/
export class Config extends Message<Config> {
/**
* @generated from field: holos.v1alpha1.PlatformForm form = 1;
*/
form?: PlatformForm;
/**
* Values are the user supplied config values organized by section.
*
* @generated from field: holos.v1alpha1.ConfigValues values = 2;
*/
values?: ConfigValues;
constructor(data?: PartialMessage<Config>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.Config";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "form", kind: "message", T: PlatformForm },
{ no: 2, name: "values", kind: "message", T: ConfigValues },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Config {
return new Config().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Config {
return new Config().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Config {
return new Config().fromJsonString(jsonString, options);
}
static equals(a: Config | PlainMessage<Config> | undefined, b: Config | PlainMessage<Config> | undefined): boolean {
return proto3.util.equals(Config, a, b);
}
}
/**
* @generated from message holos.v1alpha1.ConfigSection
*/
export class ConfigSection extends Message<ConfigSection> {
/**
* @generated from field: map<string, google.protobuf.Value> fields = 1;
*/
fields: { [key: string]: Value } = {};
constructor(data?: PartialMessage<ConfigSection>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.ConfigSection";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "fields", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: Value} },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ConfigSection {
return new ConfigSection().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ConfigSection {
return new ConfigSection().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ConfigSection {
return new ConfigSection().fromJsonString(jsonString, options);
}
static equals(a: ConfigSection | PlainMessage<ConfigSection> | undefined, b: ConfigSection | PlainMessage<ConfigSection> | undefined): boolean {
return proto3.util.equals(ConfigSection, a, b);
}
}
/**
* ConfigValues represents user defined configuration values.
*
* @generated from message holos.v1alpha1.ConfigValues
*/
export class ConfigValues extends Message<ConfigValues> {
/**
* @generated from field: map<string, holos.v1alpha1.ConfigSection> sections = 1;
*/
sections: { [key: string]: ConfigSection } = {};
constructor(data?: PartialMessage<ConfigValues>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.ConfigValues";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "sections", kind: "map", K: 9 /* ScalarType.STRING */, V: {kind: "message", T: ConfigSection} },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ConfigValues {
return new ConfigValues().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ConfigValues {
return new ConfigValues().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ConfigValues {
return new ConfigValues().fromJsonString(jsonString, options);
}
static equals(a: ConfigValues | PlainMessage<ConfigValues> | undefined, b: ConfigValues | PlainMessage<ConfigValues> | undefined): boolean {
return proto3.util.equals(ConfigValues, a, b);
}
}
/**
* @generated from message holos.v1alpha1.Platform
*/
@@ -20,25 +198,48 @@ export class Platform extends Message<Platform> {
id = "";
/**
* @generated from field: string name = 2;
*/
name = "";
/**
* @generated from field: string display_name = 3;
*/
displayName = "";
/**
* @generated from field: holos.v1alpha1.Timestamps timestamps = 4;
* @generated from field: holos.v1alpha1.Timestamps timestamps = 2;
*/
timestamps?: Timestamps;
/**
* @generated from field: holos.v1alpha1.Creator creator = 5;
* Organization ID resource owner.
*
* @generated from field: string org_id = 3;
*/
orgId = "";
/**
* name is the platform short name as a dns label.
*
* @generated from field: string name = 4;
*/
name = "";
/**
* @generated from field: string display_name = 5;
*/
displayName = "";
/**
* @generated from field: holos.v1alpha1.Creator creator = 6;
*/
creator?: Creator;
/**
* config represents the platform config form and values. Read only.
*
* @generated from field: holos.v1alpha1.Config config = 7;
*/
config?: Config;
/**
* raw_config represents the platform config form and values. Write only.
*
* @generated from field: holos.v1alpha1.RawConfig raw_config = 8;
*/
rawConfig?: RawConfig;
constructor(data?: PartialMessage<Platform>) {
super();
proto3.util.initPartial(data, this);
@@ -48,10 +249,13 @@ export class Platform extends Message<Platform> {
static readonly typeName = "holos.v1alpha1.Platform";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "timestamps", kind: "message", T: Timestamps },
{ no: 5, name: "creator", kind: "message", T: Creator },
{ no: 2, name: "timestamps", kind: "message", T: Timestamps },
{ no: 3, name: "org_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 5, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 6, name: "creator", kind: "message", T: Creator },
{ no: 7, name: "config", kind: "message", T: Config },
{ no: 8, name: "raw_config", kind: "message", T: RawConfig },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Platform {
@@ -71,6 +275,202 @@ export class Platform extends Message<Platform> {
}
}
/**
* @generated from message holos.v1alpha1.FieldConfigProps
*/
export class FieldConfigProps extends Message<FieldConfigProps> {
/**
* @generated from field: string label = 1;
*/
label = "";
/**
* @generated from field: string placeholder = 2;
*/
placeholder = "";
/**
* @generated from field: string description = 3;
*/
description = "";
/**
* @generated from field: bool required = 4;
*/
required = false;
constructor(data?: PartialMessage<FieldConfigProps>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.FieldConfigProps";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "label", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "placeholder", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "description", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "required", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): FieldConfigProps {
return new FieldConfigProps().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): FieldConfigProps {
return new FieldConfigProps().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): FieldConfigProps {
return new FieldConfigProps().fromJsonString(jsonString, options);
}
static equals(a: FieldConfigProps | PlainMessage<FieldConfigProps> | undefined, b: FieldConfigProps | PlainMessage<FieldConfigProps> | undefined): boolean {
return proto3.util.equals(FieldConfigProps, a, b);
}
}
/**
* @generated from message holos.v1alpha1.FieldConfig
*/
export class FieldConfig extends Message<FieldConfig> {
/**
* @generated from field: string key = 1;
*/
key = "";
/**
* @generated from field: string type = 2;
*/
type = "";
/**
* @generated from field: holos.v1alpha1.FieldConfigProps props = 3;
*/
props?: FieldConfigProps;
constructor(data?: PartialMessage<FieldConfig>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.FieldConfig";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "key", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "type", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "props", kind: "message", T: FieldConfigProps },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): FieldConfig {
return new FieldConfig().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): FieldConfig {
return new FieldConfig().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): FieldConfig {
return new FieldConfig().fromJsonString(jsonString, options);
}
static equals(a: FieldConfig | PlainMessage<FieldConfig> | undefined, b: FieldConfig | PlainMessage<FieldConfig> | undefined): boolean {
return proto3.util.equals(FieldConfig, a, b);
}
}
/**
* @generated from message holos.v1alpha1.ConfigFormSection
*/
export class ConfigFormSection extends Message<ConfigFormSection> {
/**
* @generated from field: string name = 1;
*/
name = "";
/**
* @generated from field: string displayName = 2;
*/
displayName = "";
/**
* @generated from field: string description = 3;
*/
description = "";
/**
* @generated from field: repeated holos.v1alpha1.FieldConfig fieldConfigs = 4;
*/
fieldConfigs: FieldConfig[] = [];
constructor(data?: PartialMessage<ConfigFormSection>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.ConfigFormSection";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "displayName", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "description", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "fieldConfigs", kind: "message", T: FieldConfig, repeated: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): ConfigFormSection {
return new ConfigFormSection().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): ConfigFormSection {
return new ConfigFormSection().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): ConfigFormSection {
return new ConfigFormSection().fromJsonString(jsonString, options);
}
static equals(a: ConfigFormSection | PlainMessage<ConfigFormSection> | undefined, b: ConfigFormSection | PlainMessage<ConfigFormSection> | undefined): boolean {
return proto3.util.equals(ConfigFormSection, a, b);
}
}
/**
* @generated from message holos.v1alpha1.PlatformFormSpec
*/
export class PlatformFormSpec extends Message<PlatformFormSpec> {
/**
* @generated from field: repeated holos.v1alpha1.ConfigFormSection sections = 1;
*/
sections: ConfigFormSection[] = [];
constructor(data?: PartialMessage<PlatformFormSpec>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.PlatformFormSpec";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "sections", kind: "message", T: ConfigFormSection, repeated: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PlatformFormSpec {
return new PlatformFormSpec().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PlatformFormSpec {
return new PlatformFormSpec().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PlatformFormSpec {
return new PlatformFormSpec().fromJsonString(jsonString, options);
}
static equals(a: PlatformFormSpec | PlainMessage<PlatformFormSpec> | undefined, b: PlatformFormSpec | PlainMessage<PlatformFormSpec> | undefined): boolean {
return proto3.util.equals(PlatformFormSpec, a, b);
}
}
/**
* @generated from message holos.v1alpha1.GetPlatformsRequest
*/
@@ -113,12 +513,7 @@ export class GetPlatformsRequest extends Message<GetPlatformsRequest> {
*/
export class GetPlatformsResponse extends Message<GetPlatformsResponse> {
/**
* @generated from field: string org_id = 1;
*/
orgId = "";
/**
* @generated from field: repeated holos.v1alpha1.Platform platforms = 2;
* @generated from field: repeated holos.v1alpha1.Platform platforms = 1;
*/
platforms: Platform[] = [];
@@ -130,8 +525,7 @@ export class GetPlatformsResponse extends Message<GetPlatformsResponse> {
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetPlatformsResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "org_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "platforms", kind: "message", T: Platform, repeated: true },
{ no: 1, name: "platforms", kind: "message", T: Platform, repeated: true },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPlatformsResponse {
@@ -151,24 +545,51 @@ export class GetPlatformsResponse extends Message<GetPlatformsResponse> {
}
}
/**
* @generated from message holos.v1alpha1.GetPlatformResponse
*/
export class GetPlatformResponse extends Message<GetPlatformResponse> {
/**
* @generated from field: holos.v1alpha1.Platform platform = 1;
*/
platform?: Platform;
constructor(data?: PartialMessage<GetPlatformResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetPlatformResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "platform", kind: "message", T: Platform },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPlatformResponse {
return new GetPlatformResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPlatformResponse {
return new GetPlatformResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPlatformResponse {
return new GetPlatformResponse().fromJsonString(jsonString, options);
}
static equals(a: GetPlatformResponse | PlainMessage<GetPlatformResponse> | undefined, b: GetPlatformResponse | PlainMessage<GetPlatformResponse> | undefined): boolean {
return proto3.util.equals(GetPlatformResponse, a, b);
}
}
/**
* @generated from message holos.v1alpha1.AddPlatformRequest
*/
export class AddPlatformRequest extends Message<AddPlatformRequest> {
/**
* @generated from field: string org_id = 1;
* @generated from field: holos.v1alpha1.Platform platform = 1;
*/
orgId = "";
/**
* @generated from field: string name = 2;
*/
name = "";
/**
* @generated from field: string display_name = 3;
*/
displayName = "";
platform?: Platform;
constructor(data?: PartialMessage<AddPlatformRequest>) {
super();
@@ -178,9 +599,7 @@ export class AddPlatformRequest extends Message<AddPlatformRequest> {
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.AddPlatformRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "org_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 1, name: "platform", kind: "message", T: Platform },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AddPlatformRequest {
@@ -200,3 +619,212 @@ export class AddPlatformRequest extends Message<AddPlatformRequest> {
}
}
/**
* @generated from message holos.v1alpha1.GetPlatformRequest
*/
export class GetPlatformRequest extends Message<GetPlatformRequest> {
/**
* @generated from field: string platform_id = 1;
*/
platformId = "";
constructor(data?: PartialMessage<GetPlatformRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetPlatformRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "platform_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPlatformRequest {
return new GetPlatformRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPlatformRequest {
return new GetPlatformRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPlatformRequest {
return new GetPlatformRequest().fromJsonString(jsonString, options);
}
static equals(a: GetPlatformRequest | PlainMessage<GetPlatformRequest> | undefined, b: GetPlatformRequest | PlainMessage<GetPlatformRequest> | undefined): boolean {
return proto3.util.equals(GetPlatformRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.MetadataName
*/
export class MetadataName extends Message<MetadataName> {
/**
* @generated from field: string name = 1;
*/
name = "";
constructor(data?: PartialMessage<MetadataName>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.MetadataName";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): MetadataName {
return new MetadataName().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): MetadataName {
return new MetadataName().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): MetadataName {
return new MetadataName().fromJsonString(jsonString, options);
}
static equals(a: MetadataName | PlainMessage<MetadataName> | undefined, b: MetadataName | PlainMessage<MetadataName> | undefined): boolean {
return proto3.util.equals(MetadataName, a, b);
}
}
/**
* @generated from message holos.v1alpha1.PlatformForm
*/
export class PlatformForm extends Message<PlatformForm> {
/**
* @generated from field: string apiVersion = 1;
*/
apiVersion = "";
/**
* @generated from field: string kind = 2;
*/
kind = "";
/**
* @generated from field: holos.v1alpha1.MetadataName metadata = 3;
*/
metadata?: MetadataName;
/**
* @generated from field: holos.v1alpha1.PlatformFormSpec spec = 4;
*/
spec?: PlatformFormSpec;
constructor(data?: PartialMessage<PlatformForm>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.PlatformForm";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "apiVersion", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "kind", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "metadata", kind: "message", T: MetadataName },
{ no: 4, name: "spec", kind: "message", T: PlatformFormSpec },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PlatformForm {
return new PlatformForm().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PlatformForm {
return new PlatformForm().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PlatformForm {
return new PlatformForm().fromJsonString(jsonString, options);
}
static equals(a: PlatformForm | PlainMessage<PlatformForm> | undefined, b: PlatformForm | PlainMessage<PlatformForm> | undefined): boolean {
return proto3.util.equals(PlatformForm, a, b);
}
}
/**
* @generated from message holos.v1alpha1.PutPlatformConfigRequest
*/
export class PutPlatformConfigRequest extends Message<PutPlatformConfigRequest> {
/**
* @generated from field: string platform_id = 1;
*/
platformId = "";
/**
* @generated from field: holos.v1alpha1.ConfigValues values = 2;
*/
values?: ConfigValues;
constructor(data?: PartialMessage<PutPlatformConfigRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.PutPlatformConfigRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "platform_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 2, name: "values", kind: "message", T: ConfigValues },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): PutPlatformConfigRequest {
return new PutPlatformConfigRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): PutPlatformConfigRequest {
return new PutPlatformConfigRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): PutPlatformConfigRequest {
return new PutPlatformConfigRequest().fromJsonString(jsonString, options);
}
static equals(a: PutPlatformConfigRequest | PlainMessage<PutPlatformConfigRequest> | undefined, b: PutPlatformConfigRequest | PlainMessage<PutPlatformConfigRequest> | undefined): boolean {
return proto3.util.equals(PutPlatformConfigRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.GetPlatformConfigRequest
*/
export class GetPlatformConfigRequest extends Message<GetPlatformConfigRequest> {
/**
* @generated from field: string platform_id = 1;
*/
platformId = "";
constructor(data?: PartialMessage<GetPlatformConfigRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.GetPlatformConfigRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "platform_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetPlatformConfigRequest {
return new GetPlatformConfigRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetPlatformConfigRequest {
return new GetPlatformConfigRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetPlatformConfigRequest {
return new GetPlatformConfigRequest().fromJsonString(jsonString, options);
}
static equals(a: GetPlatformConfigRequest | PlainMessage<GetPlatformConfigRequest> | undefined, b: GetPlatformConfigRequest | PlainMessage<GetPlatformConfigRequest> | undefined): boolean {
return proto3.util.equals(GetPlatformConfigRequest, a, b);
}
}

View File

@@ -0,0 +1,35 @@
// @generated by protoc-gen-connect-query v1.3.1 with parameter "target=ts"
// @generated from file holos/v1alpha1/system.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { MethodKind } from "@bufbuild/protobuf";
import { EmptyRequest, EmptyResponse } from "./system_pb.js";
/**
* @generated from rpc holos.v1alpha1.SystemService.SeedDatabase
*/
export const seedDatabase = {
localName: "seedDatabase",
name: "SeedDatabase",
kind: MethodKind.Unary,
I: EmptyRequest,
O: EmptyResponse,
service: {
typeName: "holos.v1alpha1.SystemService"
}
} as const;
/**
* @generated from rpc holos.v1alpha1.SystemService.DropTables
*/
export const dropTables = {
localName: "dropTables",
name: "DropTables",
kind: MethodKind.Unary,
I: EmptyRequest,
O: EmptyResponse,
service: {
typeName: "holos.v1alpha1.SystemService"
}
} as const;

View File

@@ -0,0 +1,35 @@
// @generated by protoc-gen-connect-es v1.4.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/system.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import { EmptyRequest, EmptyResponse } from "./system_pb.js";
import { MethodKind } from "@bufbuild/protobuf";
/**
* @generated from service holos.v1alpha1.SystemService
*/
export const SystemService = {
typeName: "holos.v1alpha1.SystemService",
methods: {
/**
* @generated from rpc holos.v1alpha1.SystemService.SeedDatabase
*/
seedDatabase: {
name: "SeedDatabase",
I: EmptyRequest,
O: EmptyResponse,
kind: MethodKind.Unary,
},
/**
* @generated from rpc holos.v1alpha1.SystemService.DropTables
*/
dropTables: {
name: "DropTables",
I: EmptyRequest,
O: EmptyResponse,
kind: MethodKind.Unary,
},
}
} as const;

View File

@@ -0,0 +1,70 @@
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
// @generated from file holos/v1alpha1/system.proto (package holos.v1alpha1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
* @generated from message holos.v1alpha1.EmptyRequest
*/
export class EmptyRequest extends Message<EmptyRequest> {
constructor(data?: PartialMessage<EmptyRequest>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.EmptyRequest";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): EmptyRequest {
return new EmptyRequest().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): EmptyRequest {
return new EmptyRequest().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): EmptyRequest {
return new EmptyRequest().fromJsonString(jsonString, options);
}
static equals(a: EmptyRequest | PlainMessage<EmptyRequest> | undefined, b: EmptyRequest | PlainMessage<EmptyRequest> | undefined): boolean {
return proto3.util.equals(EmptyRequest, a, b);
}
}
/**
* @generated from message holos.v1alpha1.EmptyResponse
*/
export class EmptyResponse extends Message<EmptyResponse> {
constructor(data?: PartialMessage<EmptyResponse>) {
super();
proto3.util.initPartial(data, this);
}
static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "holos.v1alpha1.EmptyResponse";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
]);
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): EmptyResponse {
return new EmptyResponse().fromBinary(bytes, options);
}
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): EmptyResponse {
return new EmptyResponse().fromJson(jsonValue, options);
}
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): EmptyResponse {
return new EmptyResponse().fromJsonString(jsonString, options);
}
static equals(a: EmptyResponse | PlainMessage<EmptyResponse> | undefined, b: EmptyResponse | PlainMessage<EmptyResponse> | undefined): boolean {
return proto3.util.equals(EmptyResponse, a, b);
}
}

View File

@@ -7,10 +7,8 @@
<span>Menu</span>
</mat-toolbar>
<mat-nav-list>
<a mat-list-item routerLink="/platform-config" routerLinkActive="active-link">Platform Config</a>
<a mat-list-item routerLink="/home" routerLinkActive="active-link">Home</a>
<a mat-list-item routerLink="/clusters" routerLinkActive="active-link">Clusters</a>
<a mat-list-item routerLink="/address-form" routerLinkActive="active-link">Address Form</a>
<a mat-list-item routerLink="/platforms" routerLinkActive="active-link">Platforms</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { PlatformService } from './platform.service';
describe('PlatformService', () => {
let service: PlatformService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(PlatformService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,61 @@
import { Inject, Injectable } from '@angular/core';
import { PlatformService as ConnectPlatformService } from '../gen/holos/v1alpha1/platform_connect';
import { Observable, filter, of, switchMap } from 'rxjs';
import { ObservableClient } from '../../connect/observable-client';
import { Config, ConfigSection, ConfigValues, GetPlatformsRequest, Platform, PutPlatformConfigRequest } from '../gen/holos/v1alpha1/platform_pb';
import { Organization } from '../gen/holos/v1alpha1/organization_pb';
import { Struct, Value } from '@bufbuild/protobuf';
export interface Section {
[field: string]: any;
}
export interface Model {
[section: string]: Section;
}
@Injectable({
providedIn: 'root'
})
export class PlatformService {
getPlatforms(org: Observable<Organization>): Observable<Platform[]> {
return org.pipe(
switchMap(org => {
const req = new GetPlatformsRequest({ orgId: org.id })
return this.client.getPlatforms(req).pipe(
switchMap(resp => { return of(resp.platforms) })
)
})
)
}
getPlatform(id: string): Observable<Platform> {
return this.client.getPlatform({ platformId: id }).pipe(
switchMap(resp => {
return of(resp.platform);
}),
filter((platform): platform is Platform => platform !== undefined),
)
}
putConfig(id: string, model: Model): Observable<Platform> {
const values = new ConfigValues
// Set string values from the model
Object.keys(model).forEach(sectionName => {
values.sections[sectionName] = new ConfigSection
Object.keys(model[sectionName]).forEach(fieldName => {
const val = new Value
val.fromJson(model[sectionName][fieldName])
values.sections[sectionName].fields[fieldName] = val
})
})
const req = new PutPlatformConfigRequest({ platformId: id, values: values })
return this.client.putPlatformConfig(req).pipe(
switchMap(resp => { return of(resp.platform) }),
filter((platform): platform is Platform => platform !== undefined),
)
}
constructor(@Inject(ConnectPlatformService) private client: ObservableClient<typeof ConnectPlatformService>) { }
}

View File

@@ -1,56 +0,0 @@
<mat-tab-group>
<mat-tab label="Organization">
<form [formGroup]="form" (ngSubmit)="onSubmit(model)">
<mat-card class="config-card">
<mat-card-header>
<mat-card-title>Organization</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Organization config values are used to derive more specific configuration values throughout the platform.</p>
<formly-form [form]="form" [fields]="fields" [model]="model">
</formly-form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" type="submit">Submit</button>
</mat-card-actions>
</mat-card>
</form>
</mat-tab>
<mat-tab label="Integrations">
<form [formGroup]="form" (ngSubmit)="onSubmit(model)">
<mat-card class="config-card">
<mat-card-header>
<mat-card-title>Integrations</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Configure integrations with your DNS and version control service providers.</p>
<formly-form [form]="form" [fields]="integrationFields" [model]="model">
</formly-form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" type="submit">Submit</button>
</mat-card-actions>
</mat-card>
</form>
</mat-tab>
<mat-tab label="Provisioner">
<form [formGroup]="form" (ngSubmit)="onSubmit(model)">
<mat-card class="config-card">
<mat-card-header>
<mat-card-title>Provisioner Cluster</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Provide the connection details used to sync secrets among platform clusters.</p>
<formly-form [form]="form" [fields]="provisionerFields" [model]="model">
</formly-form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" type="submit">Submit</button>
</mat-card-actions>
</mat-card>
</form>
</mat-tab>
<mat-tab label="OAuth Clients"></mat-tab>
</mat-tab-group>

View File

@@ -1,27 +0,0 @@
.full-width {
width: 100%;
}
.config-card {
min-width: 120px;
margin: 20px auto;
}
.mat-radio-button {
display: block;
margin: 5px 0;
}
.row {
display: flex;
flex-direction: row;
}
.col {
flex: 1;
margin-right: 20px;
}
.col:last-child {
margin-right: 0;
}

View File

@@ -1,122 +0,0 @@
import { Component } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatTabsModule } from '@angular/material/tabs';
import { MatButton } from '@angular/material/button';
import { MatCard, MatCardActions, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
import { FormlyModule, FormlyFieldConfig } from '@ngx-formly/core';
import { FormlyMaterialModule } from '@ngx-formly/material';
@Component({
selector: 'app-platform-config',
standalone: true,
imports: [
ReactiveFormsModule,
FormlyMaterialModule,
FormlyModule,
MatTabsModule,
MatCard,
MatCardHeader,
MatCardTitle,
MatCardContent,
MatCardActions,
MatButton,
],
templateUrl: './platform-config.component.html',
styleUrl: './platform-config.component.scss'
})
export class PlatformConfigComponent {
form = new FormGroup({});
model: any = {};
fields: FormlyFieldConfig[] = [
{
key: 'name',
type: 'input',
props: {
label: 'Name',
placeholder: 'example',
required: true,
description: "DNS label, e.g. 'example'"
}
},
{
key: 'domain',
type: 'input',
props: {
label: 'Domain',
placeholder: 'example.com',
required: true,
description: "DNS domain, e.g. 'example.com'"
}
},
{
key: 'displayName',
type: 'input',
props: {
label: 'Display Name',
placeholder: 'Example Organization',
required: true,
description: "Display name, e.g. 'My Organization'"
}
},
{
key: 'contactEmail',
type: 'input',
props: {
label: 'Contact Email',
placeholder: '',
required: true,
description: "Organization technical contact."
}
},
];
integrationFields: FormlyFieldConfig[] = [
{
key: 'cloudflareEmail',
type: 'input',
props: {
label: 'Cloudflare Account',
placeholder: 'example@example.com',
required: true,
description: "Cloudflare account email address."
}
},
{
key: 'githubPrimaryOrg',
type: 'input',
props: {
label: 'Github Organization',
placeholder: 'ExampleOrg',
required: true,
description: "Github organization, e.g. 'ExampleOrg'"
}
}
];
provisionerFields: FormlyFieldConfig[] = [
{
key: 'provisionerCABundle',
type: 'input',
props: {
label: 'Provisioner API CA Bundle',
placeholder: 'LS0tLS1CRUdJTiBDRVJUSUZJQXXXXXXXXXXXXXXXXXXXXXXX',
required: true,
description: "kubectl config view --minify --flatten -ojsonpath='{.clusters[0].cluster.certificate-authority-data}'"
}
},
{
key: 'provisionerURL',
type: 'input',
props: {
label: 'Provisioner API URL',
placeholder: 'https://1.2.3.4',
required: true,
description: "kubectl config view --minify --flatten -ojsonpath='{.clusters[0].cluster.server}'"
}
}
]
onSubmit(model: any) {
console.log(model);
}
}

View File

@@ -0,0 +1,25 @@
<mat-tab-group>
<mat-tab label="Detail">
<div class="grid-container">
@if (platform$ | async; as platform) {
<form [formGroup]="form" (ngSubmit)="onSubmit(model)">
@for (section of platform.config?.form?.spec?.sections; track section.name) {
<h2>{{section.displayName ? section.displayName : section.name }}</h2>
<p>{{ section.description }}</p>
<formly-form [form]="form" [fields]="section.fieldConfigs" [model]="model[section.name]"></formly-form>
}
<p></p>
<button type="submit" mat-flat-button color="primary">Submit</button>
</form>
}
</div>
</mat-tab>
<mat-tab label="Raw">
<div class="grid-container">
@if (platform$ | async; as platform) {
<pre>{{ platform | json }}</pre>
}
</div>
</mat-tab>
</mat-tab-group>

View File

@@ -0,0 +1,3 @@
.grid-container {
margin: 20px;
}

View File

@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlatformConfigComponent } from './platform-config.component';
import { PlatformDetailComponent } from './platform-detail.component';
describe('PlatformConfigComponent', () => {
let component: PlatformConfigComponent;
let fixture: ComponentFixture<PlatformConfigComponent>;
describe('PlatformDetailComponent', () => {
let component: PlatformDetailComponent;
let fixture: ComponentFixture<PlatformDetailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PlatformConfigComponent]
imports: [PlatformDetailComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PlatformConfigComponent);
fixture = TestBed.createComponent(PlatformDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -0,0 +1,69 @@
import { Component, Input, inject } from '@angular/core';
import { Observable, map, shareReplay } from 'rxjs';
import { Model, PlatformService } from '../../services/platform.service';
import { Platform } from '../../gen/holos/v1alpha1/platform_pb';
import { MatTab, MatTabGroup } from '@angular/material/tabs';
import { AsyncPipe, CommonModule } from '@angular/common';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FormlyMaterialModule } from '@ngx-formly/material';
import { FormlyModule } from '@ngx-formly/core';
import { MatButton } from '@angular/material/button';
import { MatDivider } from '@angular/material/divider';
@Component({
selector: 'app-platform-detail',
standalone: true,
imports: [
MatTabGroup,
MatTab,
AsyncPipe,
CommonModule,
FormlyModule,
ReactiveFormsModule,
FormlyMaterialModule,
MatButton,
MatDivider,
],
templateUrl: './platform-detail.component.html',
styleUrl: './platform-detail.component.scss'
})
export class PlatformDetailComponent {
private service = inject(PlatformService);
private platformId: string = "";
platform$!: Observable<Platform>;
form = new FormGroup({});
model: Model = {};
onSubmit(model: Model) {
console.log(model)
// if (this.form.valid) {
this.service.putConfig(this.platformId, model).pipe(shareReplay(1)).subscribe()
// }
}
@Input()
set id(platformId: string) {
this.platformId = platformId;
this.platform$ = this.service.getPlatform(platformId).pipe(
map(project => {
// Initialize the model container for each section of the form config
project.config?.form?.spec?.sections.forEach(section => {
this.model[section.name] = {}
})
// Load existing values into the form
const sections = project.config?.values?.sections
if (sections !== undefined) {
Object.keys(sections).forEach(sectionName => {
Object.keys(sections[sectionName].fields).forEach(fieldName => {
this.model[sectionName][fieldName] = sections[sectionName].fields[fieldName].toJson()
})
})
}
return project
}),
shareReplay(1)
)
}
}

View File

@@ -0,0 +1,11 @@
<div class="grid-container">
<mat-nav-list>
<h3 mat-subheader>Platforms</h3>
<p>Select a platform to manage.</p>
@for (platform of (platforms$ | async); track platform.id) {
<a mat-list-item [routerLink]="['/platform', platform.id]">
{{ platform.displayName ? platform.displayName : platform.name }}
</a>
}
</mat-nav-list>
</div>

View File

@@ -0,0 +1,3 @@
.grid-container {
margin: 20px;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PlatformsComponent } from './platforms.component';
describe('PlatformsComponent', () => {
let component: PlatformsComponent;
let fixture: ComponentFixture<PlatformsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PlatformsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PlatformsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,37 @@
import { Platform } from '../../gen/holos/v1alpha1/platform_pb';
import { Component, inject } from '@angular/core';
import { MatListItem, MatNavList } from '@angular/material/list';
import { Observable, filter } from 'rxjs';
import { Organization } from '../../gen/holos/v1alpha1/organization_pb';
import { OrganizationService } from '../../services/organization.service';
import { PlatformService } from '../../services/platform.service';
import { AsyncPipe, CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-platforms',
standalone: true,
imports: [
MatNavList,
MatListItem,
AsyncPipe,
CommonModule,
RouterLink,
],
templateUrl: './platforms.component.html',
styleUrl: './platforms.component.scss'
})
export class PlatformsComponent {
private orgSvc = inject(OrganizationService);
private platformSvc = inject(PlatformService);
org$!: Observable<Organization | undefined>;
platforms$!: Observable<Platform[]>;
ngOnInit(): void {
this.org$ = this.orgSvc.activeOrg();
this.platforms$ = this.platformSvc.getPlatforms(this.org$.pipe(
filter((org): org is Organization => org !== undefined)
))
}
}

View File

@@ -9,32 +9,37 @@ package handler
import (
"context"
"fmt"
"log/slog"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/server/middleware/logger"
)
// WithTx runs callbacks in a transaction as described in https://entgo.io/docs/transactions/#best-practices
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
log := logger.FromContext(ctx)
tx, err := client.Tx(ctx)
if err != nil {
return err
}
defer func() {
if v := recover(); v != nil {
slog.ErrorContext(ctx, "panic", "v", v)
log.ErrorContext(ctx, "panic", "v", v)
_ = tx.Rollback()
panic(v)
}
}()
if err := fn(tx); err != nil {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr)
log.ErrorContext(ctx, "could not roll back tx", "err", rerr)
err = fmt.Errorf("coult not roll back tx: %w: %w", rerr, err)
} else {
log.WarnContext(ctx, "rolled back failed tx", "err", err)
}
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing transaction: %w", err)
log.ErrorContext(ctx, "could not commit transaction", "err", err)
return fmt.Errorf("could not commit: %w", err)
}
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/ent/user"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server/middleware/authn"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -66,12 +67,12 @@ func (h *OrganizationHandler) CreateCallerOrganization(
ctx context.Context,
req *connect.Request[holos.CreateCallerOrganizationRequest],
) (*connect.Response[holos.GetCallerOrganizationsResponse], error) {
log := logger.FromContext(ctx)
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
// todo get user by iss, sub
dbUser, err := getUser(ctx, h.db, authnID.Email())
dbUser, err := getUser(ctx, h.db, authnID.Issuer(), authnID.Subject())
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
@@ -90,14 +91,14 @@ func (h *OrganizationHandler) CreateCallerOrganization(
if err != nil {
return err
}
dbUser, err = dbUser.Update().
AddOrganizations(org).
Save(ctx)
return err
return tx.Organization.UpdateOne(org).AddUsers(dbUser).Exec(ctx)
})
if err != nil {
return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err))
}
log = log.With("organization", org)
log.InfoContext(ctx, "created organization")
// TODO: prefetch organizations
dbOrgs, err := dbUser.QueryOrganizations().All(ctx)

View File

@@ -2,11 +2,15 @@ package handler
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"connectrpc.com/connect"
"github.com/gofrs/uuid"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/ent/organization"
"github.com/holos-run/holos/internal/ent/platform"
"github.com/holos-run/holos/internal/ent/user"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/authn"
@@ -40,16 +44,34 @@ func (h *PlatformHandler) AddPlatform(
ctx context.Context,
req *connect.Request[holos.AddPlatformRequest],
) (*connect.Response[holos.GetPlatformsResponse], error) {
dbUser, dbOrg, err := getAuthnUsersOrg(ctx, req.Msg.OrgId, h.db)
dbUser, dbOrg, err := getAuthnUsersOrg(ctx, req.Msg.Platform.OrgId, h.db)
if err != nil {
return nil, errors.Wrap(err)
}
var hf holos.PlatformForm
if len(req.Msg.Platform.RawConfig.Form) > 0 {
if err := json.Unmarshal(req.Msg.Platform.RawConfig.Form, &hf); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
}
var hv holos.ConfigValues
if len(req.Msg.Platform.RawConfig.Values) > 0 {
if err := json.Unmarshal(req.Msg.Platform.RawConfig.Values, &hv); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
}
platform, err := h.db.Platform.Create().
SetOrgID(dbOrg.ID).
SetCreatorID(dbUser.ID).
SetName(req.Msg.Name).
SetDisplayName(req.Msg.DisplayName).
SetName(req.Msg.Platform.Name).
SetDisplayName(req.Msg.Platform.DisplayName).
SetConfigForm(&hf).
SetConfigValues(&hv).
SetConfigCue(req.Msg.Platform.RawConfig.Cue).
SetConfigDefinition(req.Msg.Platform.RawConfig.Definition).
Save(ctx)
if err != nil {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
@@ -61,11 +83,114 @@ func (h *PlatformHandler) AddPlatform(
return resp, nil
}
func (h *PlatformHandler) getPlatform(ctx context.Context, id string, uid authn.Identity) (*ent.Platform, error) {
platformID, err := uuid.FromString(id)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
p, err := h.db.Platform.Query().
Where(platform.ID(platformID)).
Where(platform.HasOrganizationWith(
organization.HasUsersWith(
user.Iss(uid.Issuer()),
user.Sub(uid.Subject()),
))).
Only(ctx)
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
} else {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
}
return p, nil
}
func (h *PlatformHandler) GetPlatform(ctx context.Context, req *connect.Request[holos.GetPlatformRequest]) (*connect.Response[holos.GetPlatformResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
p, err := h.getPlatform(ctx, req.Msg.PlatformId, authnID)
if err != nil {
return nil, errors.Wrap(err)
}
return connect.NewResponse(&holos.GetPlatformResponse{Platform: PlatformToRPC(p)}), nil
}
func (h *PlatformHandler) PutPlatformConfig(ctx context.Context, req *connect.Request[holos.PutPlatformConfigRequest]) (*connect.Response[holos.GetPlatformResponse], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
id, err := uuid.FromString(req.Msg.PlatformId)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Wrap(err))
}
// Get the platform so we can validate the values.
p, err := h.db.Platform.Query().
Where(platform.ID(id)).
Where(platform.HasOrganizationWith(
organization.HasUsersWith(
user.Iss(authnID.Issuer()),
user.Sub(authnID.Subject()),
))).
Only(ctx)
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
} else {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
}
slog.WarnContext(ctx, "todo: validate the platform config against cue definitions", "action", "todo", "cue", len(p.ConfigCue))
up, err := h.db.Platform.UpdateOneID(id).
Where(platform.HasOrganizationWith(
organization.HasUsersWith(
user.Iss(authnID.Issuer()),
user.Sub(authnID.Subject()),
))).
SetConfigValues(req.Msg.Values).
Save(ctx)
if err != nil {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
return connect.NewResponse(&holos.GetPlatformResponse{Platform: PlatformToRPC(up)}), nil
}
func (h *PlatformHandler) GetConfig(ctx context.Context, req *connect.Request[holos.GetPlatformConfigRequest]) (*connect.Response[holos.ConfigValues], error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
p, err := h.getPlatform(ctx, req.Msg.PlatformId, authnID)
if err != nil {
return nil, errors.Wrap(err)
}
return connect.NewResponse(p.ConfigValues), nil
}
func PlatformToRPC(platform *ent.Platform) *holos.Platform {
return &holos.Platform{
Id: platform.ID.String(),
Name: platform.Name,
DisplayName: platform.DisplayName,
OrgId: platform.OrgID.String(),
Config: &holos.Config{
Form: platform.ConfigForm,
Values: platform.ConfigValues,
},
Timestamps: &holos.Timestamps{
CreatedAt: timestamppb.New(platform.CreatedAt),
UpdatedAt: timestamppb.New(platform.UpdatedAt),
@@ -101,6 +226,7 @@ func getAuthnUsersOrg(ctx context.Context, orgID string, db *ent.Client) (*ent.U
return nil, nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
// Check the user is a member of the organization.
var reqDBOrg *ent.Organization
wantOrgIDs := make([]uuid.UUID, 0, len(dbUser.Edges.Organizations))
for _, org := range dbUser.Edges.Organizations {
@@ -131,7 +257,6 @@ func getPlatformsResponse(reqDBOrg *ent.Organization) *connect.Response[holos.Ge
}
return connect.NewResponse(&holos.GetPlatformsResponse{
OrgId: reqDBOrg.ID.String(),
Platforms: rpcPlatforms,
})
}

View File

@@ -0,0 +1,215 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"strings"
"connectrpc.com/connect"
"github.com/holos-run/holos/internal/ent"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/server/middleware/authn"
"github.com/holos-run/holos/internal/server/middleware/logger"
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
)
const AdminEmail = "jeff@openinfrastructure.co"
// NewSystemHandler returns a new SystemService implementation.
func NewSystemHandler(db *ent.Client) *SystemHandler {
return &SystemHandler{db: db}
}
// SystemHandler implements the PlatformService interface.
type SystemHandler struct {
db *ent.Client
}
func (h *SystemHandler) checkAdmin(ctx context.Context) error {
authnID, err := authn.FromContext(ctx)
if err != nil {
return connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
if authnID.Email() != AdminEmail {
err := fmt.Errorf("not an admin:\n\thave (%+v)\n\twant (%+v)", authnID.Email(), AdminEmail)
return connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
return nil
}
func (h *SystemHandler) DropTables(ctx context.Context, req *connect.Request[holos.EmptyRequest]) (*connect.Response[holos.EmptyResponse], error) {
if err := h.checkAdmin(ctx); err != nil {
return nil, err
}
log := logger.FromContext(ctx)
if err := WithTx(ctx, h.db, func(tx *ent.Tx) (err error) {
var n int
if n, err = tx.Platform.Delete().Exec(ctx); err != nil {
return errors.Wrap(err)
}
log.WarnContext(ctx, "deleted platforms", "count", n)
if n, err = tx.Organization.Delete().Exec(ctx); err != nil {
return errors.Wrap(err)
}
log.WarnContext(ctx, "deleted organizations", "count", n)
if n, err = tx.User.Delete().Exec(ctx); err != nil {
return errors.Wrap(err)
}
log.WarnContext(ctx, "deleted users", "count", n)
return nil
}); err != nil {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
return connect.NewResponse(&holos.EmptyResponse{}), nil
}
func (h *SystemHandler) SeedDatabase(ctx context.Context, req *connect.Request[holos.EmptyRequest]) (*connect.Response[holos.EmptyResponse], error) {
if err := h.checkAdmin(ctx); err != nil {
return nil, err
}
if err := WithTx(ctx, h.db, func(tx *ent.Tx) (err error) {
jeff, err := tx.User.Create().
SetEmail("jeff@openinfrastructure.co").
SetIss("https://login.ois.run").
SetSub("261773693724656988").
SetName("Jeff McCune").
Save(ctx)
if err != nil {
return errors.Wrap(err)
}
nate, err := tx.User.Create().
SetEmail("nate@openinfrastructure.co").
SetIss("https://login.ois.run").
SetSub("261775487611699776").
SetName("Nate McCurdy").
Save(ctx)
if err != nil {
return errors.Wrap(err)
}
gary, err := tx.User.Create().
SetEmail("gary@openinfrastructure.co").
SetIss("https://login.ois.run").
SetSub("261775531836441152").
SetName("Gary Larizza").
Save(ctx)
if err != nil {
return errors.Wrap(err)
}
// Create the org
org, err := tx.Organization.Create().
SetName("ois").
SetDisplayName("Open Infrastructure Services").
SetCreator(jeff).
Save(ctx)
if err != nil {
return errors.Wrap(err)
}
// Add org memebers
org, err = org.Update().AddUsers(jeff, gary, nate).Save(ctx)
if err != nil {
return errors.Wrap(err)
}
var hf holos.PlatformForm
if err := json.Unmarshal([]byte(BareForm), &hf); err != nil {
return errors.Wrap(err)
}
// Add a platform
err = tx.Platform.Create().
SetName("bare").
SetDisplayName("Bare Platform").
SetConfigForm(&hf).
SetCreator(jeff).
SetOrgID(org.ID).
Exec(ctx)
if err != nil {
return errors.Wrap(err)
}
stuff := []string{"Jeff", "Gary", "Nate"}
for _, name := range stuff {
err := tx.Platform.Create().
SetName(strings.ToLower(name)).
SetDisplayName(name + "'s Platform").
SetConfigForm(&hf).
SetCreator(jeff).
SetOrgID(org.ID).
Exec(ctx)
if err != nil {
return errors.Wrap(err)
}
}
return nil
}); err != nil {
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
}
return connect.NewResponse(&holos.EmptyResponse{}), nil
}
const BareForm = `{
"kind": "PlatformForm",
"spec": {
"sections": [
{
"name": "org",
"description": "Organization config values are used to derive more specific configuration values throughout the platform.",
"displayName": "Organization",
"fieldConfigs": [
{
"key": "name",
"type": "input",
"props": {
"label": "Name",
"required": true,
"description": "DNS label, e.g. 'example'",
"placeholder": "example"
}
},
{
"key": "domain",
"type": "input",
"props": {
"label": "Domain",
"required": true,
"description": "DNS domain, e.g. 'example.com'",
"placeholder": "example.com"
}
},
{
"key": "displayName",
"type": "input",
"props": {
"label": "Display Name",
"required": true,
"description": "Display name, e.g. 'Example Organization'",
"placeholder": "Example Organization"
}
},
{
"key": "contactEmail",
"type": "input",
"props": {
"label": "Contact Email",
"required": true,
"description": "Technical contact email address",
"placeholder": "platform-team@example.com"
}
}
]
}
]
},
"metadata": {
"name": "bare"
},
"apiVersion": "forms.holos.run/v1alpha1"
}`

View File

@@ -56,7 +56,7 @@ func (h *UserHandler) GetCallerUser(
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
}
dbUser, err := getUser(ctx, h.db, authnID.Email())
dbUser, err := getUser(ctx, h.db, authnID.Issuer(), authnID.Subject())
if err != nil {
if ent.MaskNotFound(err) == nil {
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
@@ -68,10 +68,7 @@ func (h *UserHandler) GetCallerUser(
return res, nil
}
func (h *UserHandler) CreateCallerUser(
ctx context.Context,
req *connect.Request[holos.CreateCallerUserRequest],
) (*connect.Response[holos.CreateCallerUserResponse], error) {
func (h *UserHandler) createCallerUser(ctx context.Context) (*ent.User, error) {
authnID, err := authn.FromContext(ctx)
if err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
@@ -87,6 +84,18 @@ func (h *UserHandler) CreateCallerUser(
return nil, err
}
return createdUser, nil
}
func (h *UserHandler) CreateCallerUser(
ctx context.Context,
req *connect.Request[holos.CreateCallerUserRequest],
) (*connect.Response[holos.CreateCallerUserResponse], error) {
createdUser, err := h.createCallerUser(ctx)
if err != nil {
return nil, err
}
res := connect.NewResponse(&holos.CreateCallerUserResponse{
User: UserToRPC(createdUser),
})
@@ -107,11 +116,16 @@ func UserToRPC(u *ent.User) *holos.User {
return &iamUser
}
func getUser(ctx context.Context, client *ent.Client, email string) (*ent.User, error) {
func getUser(ctx context.Context, client *ent.Client, iss string, sub string) (*ent.User, error) {
log := logger.FromContext(ctx)
user, err := client.User.Query().Where(user.Email(email)).Only(ctx)
user, err := client.User.Query().
Where(
user.Iss(iss),
user.Sub(sub),
).
Only(ctx)
if err != nil {
log.DebugContext(ctx, "could not get user", "err", err, "email", email)
log.DebugContext(ctx, "could not get user", "err", err, "iss", iss, "sub", sub)
return nil, errors.Wrap(err)
}
return user, nil

View File

@@ -115,11 +115,13 @@ func (s *Server) registerConnectRpc() error {
s.handle(holosconnect.NewUserServiceHandler(handler.NewUserHandler(s.db), opts))
s.handle(holosconnect.NewOrganizationServiceHandler(handler.NewOrganizationHandler(s.db), opts))
s.handle(holosconnect.NewPlatformServiceHandler(handler.NewPlatformHandler(s.db), opts))
s.handle(holosconnect.NewSystemServiceHandler(handler.NewSystemHandler(s.db), opts))
reflector := grpcreflect.NewStaticReflector(
holosconnect.UserServiceName,
holosconnect.OrganizationServiceName,
holosconnect.PlatformServiceName,
holosconnect.SystemServiceName,
)
s.mux.Handle(grpcreflect.NewHandlerV1(reflector))

View File

@@ -9,20 +9,73 @@ import "buf/validate/validate.proto";
import "holos/v1alpha1/timestamps.proto";
import "holos/v1alpha1/organization.proto";
import "holos/v1alpha1/user.proto";
import "google/protobuf/any.proto";
import "google/protobuf/struct.proto";
// For validation, see the [Standard constraints](https://github.com/bufbuild/protovalidate/blob/main/docs/standard-constraints.md)
// RawConfig represents the raw form configuration as opaque bytes. Used for input.
message RawConfig {
bytes form = 1;
bytes values = 2;
bytes cue = 3;
string definition = 4;
}
message Config {
PlatformForm form = 1;
// Values are the user supplied config values organized by section.
ConfigValues values = 2;
}
message ConfigSection {
map<string, google.protobuf.Value> fields = 1;
}
// ConfigValues represents user defined configuration values.
message ConfigValues {
map<string, ConfigSection> sections = 1;
}
message Platform {
// Unique id assigned by the server.
string id = 1 [(buf.validate.field).string.uuid = true];
string name = 2 [(buf.validate.field).string.max_len = 100];
string display_name = 3 [(buf.validate.field).string.max_len = 100];
string id = 1;
Timestamps timestamps = 2;
// platform spec here?
// platform spec form config here?
// Organization ID resource owner.
string org_id = 3 [(buf.validate.field).string.uuid = true];
// name is the platform short name as a dns label.
string name = 4 [(buf.validate.field).string.max_len = 100];
string display_name = 5 [(buf.validate.field).string.max_len = 100];
Creator creator = 6;
// config represents the platform config form and values. Read only.
Config config = 7;
// raw_config represents the platform config form and values. Write only.
RawConfig raw_config = 8;
}
Timestamps timestamps = 4;
Creator creator = 5;
message FieldConfigProps {
string label = 1;
string placeholder = 2;
string description = 3;
bool required = 4;
}
message FieldConfig {
string key = 1;
string type = 2;
FieldConfigProps props = 3;
}
message ConfigFormSection {
string name = 1;
string displayName = 2;
string description = 3;
repeated FieldConfig fieldConfigs = 4;
}
message PlatformFormSpec {
repeated ConfigFormSection sections = 1;
}
message GetPlatformsRequest {
@@ -30,17 +83,46 @@ message GetPlatformsRequest {
}
message GetPlatformsResponse {
string org_id = 1 [(buf.validate.field).string.uuid = true];
repeated Platform platforms = 2;
repeated Platform platforms = 1;
}
message GetPlatformResponse {
Platform platform = 1;
}
message AddPlatformRequest {
string org_id = 1 [(buf.validate.field).string.uuid = true];
string name = 2 [(buf.validate.field).string.max_len = 100];
string display_name = 3 [(buf.validate.field).string.max_len = 100];
Platform platform = 1;
}
message GetPlatformRequest {
string platform_id = 1 [(buf.validate.field).string.uuid = true];
}
message MetadataName {
string name = 1;
}
message PlatformForm {
string apiVersion = 1;
string kind = 2;
MetadataName metadata = 3;
PlatformFormSpec spec = 4;
}
message PutPlatformConfigRequest {
string platform_id = 1 [(buf.validate.field).string.uuid = true];
ConfigValues values = 2;
}
message GetPlatformConfigRequest {
string platform_id = 1 [(buf.validate.field).string.uuid = true];
}
service PlatformService {
rpc GetPlatforms(GetPlatformsRequest) returns (GetPlatformsResponse) {}
rpc AddPlatform(AddPlatformRequest) returns (GetPlatformsResponse) {}
rpc GetPlatforms(GetPlatformsRequest) returns (GetPlatformsResponse) {}
rpc GetPlatform(GetPlatformRequest) returns (GetPlatformResponse) {}
rpc PutPlatformConfig(PutPlatformConfigRequest) returns (GetPlatformResponse) {}
// GetConfig provides the unmarshalled config values for use with CUE
rpc GetConfig(GetPlatformConfigRequest) returns (ConfigValues) {}
}

View File

@@ -0,0 +1,20 @@
syntax = "proto3";
package holos.v1alpha1;
option go_package = "github.com/holos-run/holos/service/gen/holos/v1alpha1;holos";
// git clone https://github.com/bufbuild/protovalidate then add <parent>/protovalidate/proto/protovalidate to your editor proto search path
import "buf/validate/validate.proto";
import "holos/v1alpha1/timestamps.proto";
import "holos/v1alpha1/user.proto";
// For validation, see the [Standard constraints](https://github.com/bufbuild/protovalidate/blob/main/docs/standard-constraints.md)
message EmptyRequest {}
message EmptyResponse {}
service SystemService {
rpc SeedDatabase(EmptyRequest) returns (EmptyResponse) {}
rpc DropTables(EmptyRequest) returns (EmptyResponse) {}
}

View File

@@ -1 +1 @@
70
72

View File

@@ -1 +1 @@
2
1