mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 08:44:58 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f8eab816 | ||
|
|
aee15f95e2 | ||
|
|
1c540ac375 | ||
|
|
5b0e883ac9 | ||
|
|
9a2519af71 | ||
|
|
9b9ff601c0 | ||
|
|
2f798296dc | ||
|
|
2b2ff63cad | ||
|
|
3b135c09f3 |
@@ -11,14 +11,14 @@ plugins:
|
||||
out: service/gen
|
||||
opt: paths=source_relative
|
||||
- plugin: es
|
||||
out: internal/frontend/holos/gen
|
||||
out: internal/frontend/holos/src/app/gen
|
||||
opt:
|
||||
- target=ts
|
||||
- plugin: connect-es
|
||||
out: internal/frontend/holos/gen
|
||||
out: internal/frontend/holos/src/app/gen
|
||||
opt:
|
||||
- target=ts
|
||||
- plugin: connect-query
|
||||
out: internal/frontend/holos/gen
|
||||
out: internal/frontend/holos/src/app/gen
|
||||
opt:
|
||||
- target=ts
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,6 +5,7 @@ go 1.21.5
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240401165935-b983156c5e99.1
|
||||
connectrpc.com/connect v1.16.0
|
||||
connectrpc.com/grpcreflect v1.2.0
|
||||
connectrpc.com/validate v0.1.0
|
||||
cuelang.org/go v0.8.0
|
||||
entgo.io/ent v0.13.1
|
||||
@@ -42,7 +43,6 @@ require (
|
||||
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
connectrpc.com/grpcreflect v1.2.0 // indirect
|
||||
connectrpc.com/otelconnect v0.7.0 // indirect
|
||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
|
||||
@@ -103,13 +103,42 @@ spec:
|
||||
hosts:
|
||||
- '{developer}.app.dev.k2.holos.run'
|
||||
http:
|
||||
- route:
|
||||
- name: "coffee-ui"
|
||||
match:
|
||||
- uri:
|
||||
prefix: "/ui"
|
||||
route:
|
||||
- destination:
|
||||
host: coffee
|
||||
port:
|
||||
number: 4200
|
||||
- name: "holos-api"
|
||||
route:
|
||||
- destination:
|
||||
host: '{name}'
|
||||
port:
|
||||
number: {listen_port}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: coffee
|
||||
spec:
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 4200
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: coffee
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.2.21
|
||||
ports:
|
||||
- port: 4200
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: holos
|
||||
|
||||
86
internal/frontend/holos/package-lock.json
generated
86
internal/frontend/holos/package-lock.json
generated
@@ -18,7 +18,7 @@
|
||||
"@angular/platform-browser": "^17.3.0",
|
||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||
"@angular/router": "^17.3.0",
|
||||
"@bufbuild/protobuf": "^1.8.0",
|
||||
"@bufbuild/protobuf": "^1.9.0",
|
||||
"@connectrpc/connect": "^1.4.0",
|
||||
"@connectrpc/connect-query": "^1.3.1",
|
||||
"@connectrpc/connect-web": "^1.4.0",
|
||||
@@ -30,8 +30,8 @@
|
||||
"@angular-devkit/build-angular": "^17.3.4",
|
||||
"@angular/cli": "^17.3.4",
|
||||
"@angular/compiler-cli": "^17.3.0",
|
||||
"@bufbuild/buf": "^1.30.1",
|
||||
"@bufbuild/protoc-gen-es": "^1.8.0",
|
||||
"@bufbuild/buf": "^1.31.0",
|
||||
"@bufbuild/protoc-gen-es": "^1.9.0",
|
||||
"@connectrpc/protoc-gen-connect-es": "^1.4.0",
|
||||
"@connectrpc/protoc-gen-connect-query": "^1.3.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
@@ -2337,9 +2337,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.30.1.tgz",
|
||||
"integrity": "sha512-9VVvrXBCWUiH8ToccqDfPRuTiPXSbHmSkL8XPlMpUhpJIlm01m4/Vzbc5FJL1yuk3e1rdBGCF6I9Obs9NsILzg==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.31.0.tgz",
|
||||
"integrity": "sha512-kM/eueGkp0NDo8p8B6GXr1MdCzf4w8zEV1gbEiDlaLYDoyeHGLtlf5jF/hrb6MsvCccy3x7cc+cj4Wn/DmoR2g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
@@ -2351,18 +2351,18 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@bufbuild/buf-darwin-arm64": "1.30.1",
|
||||
"@bufbuild/buf-darwin-x64": "1.30.1",
|
||||
"@bufbuild/buf-linux-aarch64": "1.30.1",
|
||||
"@bufbuild/buf-linux-x64": "1.30.1",
|
||||
"@bufbuild/buf-win32-arm64": "1.30.1",
|
||||
"@bufbuild/buf-win32-x64": "1.30.1"
|
||||
"@bufbuild/buf-darwin-arm64": "1.31.0",
|
||||
"@bufbuild/buf-darwin-x64": "1.31.0",
|
||||
"@bufbuild/buf-linux-aarch64": "1.31.0",
|
||||
"@bufbuild/buf-linux-x64": "1.31.0",
|
||||
"@bufbuild/buf-win32-arm64": "1.31.0",
|
||||
"@bufbuild/buf-win32-x64": "1.31.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-darwin-arm64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.30.1.tgz",
|
||||
"integrity": "sha512-FRgf+x4V4s9Z1wH2xHdP8+1AYtil1GCmMjzKf/4AQ+eaUpoLfipSIsVYiBrnpcRxEPe9UMVzwNjKtPak/szwPw==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.31.0.tgz",
|
||||
"integrity": "sha512-C0jArGS/SW0jfpBBmG6xEhUBWQTsGInnPr7y44WYWNS/U5OnnWPJtYQ7xbH0mzYDMx7sZVRV0FKvPO0FPKS+hA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2376,9 +2376,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-darwin-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-kE0ne45zE7lSdv9WxPVhapwu627WMbWmWCzqSxzYr8sWDLqiAuw+XvO9/mHGdPWcMhV4lMX6tutitd9PPVxK8A==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.31.0.tgz",
|
||||
"integrity": "sha512-oBTe1T4l2WSukAG+9YS7VHID4N1CuSvAxGBfuzpFQrjjiQZaaTfYuLqqVP6408MyCN7X/LOjfCekR1QToVweNw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2392,9 +2392,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-linux-aarch64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.30.1.tgz",
|
||||
"integrity": "sha512-kVV9Sl0GwZiQkMOXJiuwuU+gIHe6AWcYBMRMmuW55sY0ePZNXBmRGt4k5W4ijy98O6pnY3ao+n9ne0KwiD9MVA==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.31.0.tgz",
|
||||
"integrity": "sha512-UN9KsTuO9YS5Vefj/CaqX1wO+hvc3AyGElxzOHMc7S3MWEuqSAFOhxu5I7CyOr2/yoZO2qZPPR29HuzmQsb2+w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2408,9 +2408,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-linux-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-RacDbQJYNwqRlMESa/rLHprfUVa8Wu1/cmcqS29Fyt/cGzs0G8sNcQzQ87HYFIS9cSlSPl6vWL0x8JqQRp68lQ==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.31.0.tgz",
|
||||
"integrity": "sha512-4uKq5Iu5tNEFE3Mz9a52KFCaywg5xLqwhN6Kf4kAk34kxWJgQ8D3WFe9ZpXHzH7Lj00u3Q+V/3vKCVATHR3tkw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2424,9 +2424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-win32-arm64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.30.1.tgz",
|
||||
"integrity": "sha512-ndp/qb5M6yrSzcnMI0j4jjAuDKa7zHBFc187FwyDb3v63rvyQeYqncHb0leT5ZWqfNggJT4vXIH6QnH82PfDQw==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.31.0.tgz",
|
||||
"integrity": "sha512-jz7GenlNsqwbC3qaHcBHBO35MycZ1gV8OUSRp/wTXGgZsEZzAyw335JA2NWL+5LaI8cF+CsYd6/uuWzKkdCKTQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2440,9 +2440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/buf-win32-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-1kmIY6oKLKZ4zIQVNG60GRDp+vKSZdaim7wRejOtgEDuWXhIuErlnGbpstypU8FO+OV3SeFUJNOJ8tLOYd3PvQ==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.31.0.tgz",
|
||||
"integrity": "sha512-gw7p3PYam0g7hNwIhNphua5P8GBZczginoWNK3jk5sGVv0TzWOdHrjkVVhkc3DJbRZEg10ExGI3qRfCqiX1IHw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2456,18 +2456,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.8.0.tgz",
|
||||
"integrity": "sha512-qR9FwI8QKIveDnUYutvfzbC21UZJJryYrLuZGjeZ/VGz+vXelUkK+xgkOHsvPEdYEdxtgUUq4313N8QtOehJ1Q=="
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.9.0.tgz",
|
||||
"integrity": "sha512-W7gp8Q/v1NlCZLsv8pQ3Y0uCu/SHgXOVFK+eUluUKWXmsb6VHkpNx0apdOWWcDbB9sJoKeP8uPrjmehJz6xETQ=="
|
||||
},
|
||||
"node_modules/@bufbuild/protoc-gen-es": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-1.8.0.tgz",
|
||||
"integrity": "sha512-jnvBKwHq3o/iOgfKxaxn5Za7ay4oAs8KWgoHiDc9Fsb0g+/d1z+mHlHvmevOiCPcVZsnH6V3LImOJvGStPONpA==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-1.9.0.tgz",
|
||||
"integrity": "sha512-LJy1nC3Jsfdhs9v48P7qF6YXIqh+usFhXSVzJDTmw0yKjxQ3CKBNISRtaMql/g9hb1MLRU6unHCcFfdz4HSO/Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.8.0",
|
||||
"@bufbuild/protoplugin": "1.8.0"
|
||||
"@bufbuild/protobuf": "^1.9.0",
|
||||
"@bufbuild/protoplugin": "1.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"protoc-gen-es": "bin/protoc-gen-es"
|
||||
@@ -2476,7 +2476,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@bufbuild/protobuf": "1.8.0"
|
||||
"@bufbuild/protobuf": "1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@bufbuild/protobuf": {
|
||||
@@ -2485,12 +2485,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protoplugin": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-1.8.0.tgz",
|
||||
"integrity": "sha512-Pb89cTshW+I577qh27VvxGYvZEvQ3zJ8La1OfzPCKugP9d4A4P65WStkAY+aSCiDHk68m1/+mtBb6elfiLPuFg==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-1.9.0.tgz",
|
||||
"integrity": "sha512-/mxMiGs5h78RUHT7v4+mv0Wt0gyRf/SOS5PLzKEg2sclEAlFPbXfZ8HjlvxJpXZP/YpP3HvsW/mil3E69G0mXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "1.8.0",
|
||||
"@bufbuild/protobuf": "1.9.0",
|
||||
"@typescript/vfs": "^1.4.0",
|
||||
"typescript": "4.5.2"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@angular/platform-browser": "^17.3.0",
|
||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||
"@angular/router": "^17.3.0",
|
||||
"@bufbuild/protobuf": "^1.8.0",
|
||||
"@bufbuild/protobuf": "^1.9.0",
|
||||
"@connectrpc/connect": "^1.4.0",
|
||||
"@connectrpc/connect-query": "^1.3.1",
|
||||
"@connectrpc/connect-web": "^1.4.0",
|
||||
@@ -32,8 +32,8 @@
|
||||
"@angular-devkit/build-angular": "^17.3.4",
|
||||
"@angular/cli": "^17.3.4",
|
||||
"@angular/compiler-cli": "^17.3.0",
|
||||
"@bufbuild/buf": "^1.30.1",
|
||||
"@bufbuild/protoc-gen-es": "^1.8.0",
|
||||
"@bufbuild/buf": "^1.31.0",
|
||||
"@bufbuild/protoc-gen-es": "^1.9.0",
|
||||
"@connectrpc/protoc-gen-connect-es": "^1.4.0",
|
||||
"@connectrpc/protoc-gen-connect-query": "^1.3.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
@@ -45,4 +45,4 @@
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.4.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
// import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
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';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(routes), provideAnimationsAsync()]
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideAnimationsAsync(),
|
||||
// provideHttpClient(withFetch()),
|
||||
provideClient(UserService),
|
||||
provideClient(OrganizationService),
|
||||
importProvidersFrom(
|
||||
ConnectModule.forRoot({
|
||||
baseUrl: window.location.origin
|
||||
}),
|
||||
),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { MethodKind } from "@bufbuild/protobuf";
|
||||
import { CreateCallerOrganizationRequest, CreateCallerOrganizationResponse, GetCallerOrganizationsRequest, GetCallerOrganizationsResponse } from "./organization_pb.js";
|
||||
import { CreateCallerOrganizationRequest, GetCallerOrganizationsRequest, GetCallerOrganizationsResponse } from "./organization_pb.js";
|
||||
|
||||
/**
|
||||
* @generated from rpc holos.v1alpha1.OrganizationService.GetCallerOrganizations
|
||||
@@ -28,7 +28,7 @@ export const createCallerOrganization = {
|
||||
name: "CreateCallerOrganization",
|
||||
kind: MethodKind.Unary,
|
||||
I: CreateCallerOrganizationRequest,
|
||||
O: CreateCallerOrganizationResponse,
|
||||
O: GetCallerOrganizationsResponse,
|
||||
service: {
|
||||
typeName: "holos.v1alpha1.OrganizationService"
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { CreateCallerOrganizationRequest, CreateCallerOrganizationResponse, GetCallerOrganizationsRequest, GetCallerOrganizationsResponse } from "./organization_pb.js";
|
||||
import { CreateCallerOrganizationRequest, GetCallerOrganizationsRequest, GetCallerOrganizationsResponse } from "./organization_pb.js";
|
||||
import { MethodKind } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ export const OrganizationService = {
|
||||
createCallerOrganization: {
|
||||
name: "CreateCallerOrganization",
|
||||
I: CreateCallerOrganizationRequest,
|
||||
O: CreateCallerOrganizationResponse,
|
||||
O: GetCallerOrganizationsResponse,
|
||||
kind: MethodKind.Unary,
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v1.8.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
|
||||
// @generated from file holos/v1alpha1/organization.proto (package holos.v1alpha1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
@@ -170,46 +170,3 @@ export class CreateCallerOrganizationRequest extends Message<CreateCallerOrganiz
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message holos.v1alpha1.CreateCallerOrganizationResponse
|
||||
*/
|
||||
export class CreateCallerOrganizationResponse extends Message<CreateCallerOrganizationResponse> {
|
||||
/**
|
||||
* @generated from field: holos.v1alpha1.User user = 1;
|
||||
*/
|
||||
user?: User;
|
||||
|
||||
/**
|
||||
* @generated from field: repeated holos.v1alpha1.Organization organizations = 2;
|
||||
*/
|
||||
organizations: Organization[] = [];
|
||||
|
||||
constructor(data?: PartialMessage<CreateCallerOrganizationResponse>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
}
|
||||
|
||||
static readonly runtime: typeof proto3 = proto3;
|
||||
static readonly typeName = "holos.v1alpha1.CreateCallerOrganizationResponse";
|
||||
static readonly fields: FieldList = proto3.util.newFieldList(() => [
|
||||
{ no: 1, name: "user", kind: "message", T: User },
|
||||
{ no: 2, name: "organizations", kind: "message", T: Organization, repeated: true },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CreateCallerOrganizationResponse {
|
||||
return new CreateCallerOrganizationResponse().fromBinary(bytes, options);
|
||||
}
|
||||
|
||||
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CreateCallerOrganizationResponse {
|
||||
return new CreateCallerOrganizationResponse().fromJson(jsonValue, options);
|
||||
}
|
||||
|
||||
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CreateCallerOrganizationResponse {
|
||||
return new CreateCallerOrganizationResponse().fromJsonString(jsonString, options);
|
||||
}
|
||||
|
||||
static equals(a: CreateCallerOrganizationResponse | PlainMessage<CreateCallerOrganizationResponse> | undefined, b: CreateCallerOrganizationResponse | PlainMessage<CreateCallerOrganizationResponse> | undefined): boolean {
|
||||
return proto3.util.equals(CreateCallerOrganizationResponse, a, b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v1.8.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
|
||||
// @generated from file holos/v1alpha1/timestamps.proto (package holos.v1alpha1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protoc-gen-es v1.8.0 with parameter "target=ts"
|
||||
// @generated by protoc-gen-es v1.9.0 with parameter "target=ts"
|
||||
// @generated from file holos/v1alpha1/user.proto (package holos.v1alpha1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
@@ -205,6 +205,21 @@ export class Claims extends Message<Claims> {
|
||||
*/
|
||||
groups: string[] = [];
|
||||
|
||||
/**
|
||||
* @generated from field: string given_name = 7;
|
||||
*/
|
||||
givenName = "";
|
||||
|
||||
/**
|
||||
* @generated from field: string family_name = 8;
|
||||
*/
|
||||
familyName = "";
|
||||
|
||||
/**
|
||||
* @generated from field: string picture = 9;
|
||||
*/
|
||||
picture = "";
|
||||
|
||||
constructor(data?: PartialMessage<Claims>) {
|
||||
super();
|
||||
proto3.util.initPartial(data, this);
|
||||
@@ -219,6 +234,9 @@ export class Claims extends Message<Claims> {
|
||||
{ no: 4, name: "email_verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
|
||||
{ no: 5, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 6, name: "groups", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
|
||||
{ no: 7, name: "given_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 8, name: "family_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
{ no: 9, name: "picture", kind: "scalar", T: 9 /* ScalarType.STRING */ },
|
||||
]);
|
||||
|
||||
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Claims {
|
||||
@@ -3,7 +3,9 @@
|
||||
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
||||
[mode]="(isHandset$ | async) ? 'over' : 'side'"
|
||||
[opened]="(isHandset$ | async) === false">
|
||||
<mat-toolbar>Menu</mat-toolbar>
|
||||
<mat-toolbar>
|
||||
<span>Menu</span>
|
||||
</mat-toolbar>
|
||||
<mat-nav-list>
|
||||
<a mat-list-item routerLink="/home" routerLinkActive="active-link">Home</a>
|
||||
<a mat-list-item routerLink="/clusters" routerLinkActive="active-link">Clusters</a>
|
||||
@@ -21,6 +23,15 @@
|
||||
</button>
|
||||
}
|
||||
<span>Holos</span>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<span>
|
||||
@if (org$ | async; as org) {
|
||||
<button mat-button (click)="refreshOrg()">
|
||||
{{ org.displayName }}
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
<app-profile-button [claims$]="claims$"></app-profile-button>
|
||||
</mat-toolbar>
|
||||
<!-- Add Content Here -->
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -15,3 +15,22 @@
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
&.image {
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
@@ -9,6 +9,12 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { Claims } from '../gen/holos/v1alpha1/user_pb';
|
||||
import { ProfileButtonComponent } from '../profile-button/profile-button.component';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { Organization } from '../gen/holos/v1alpha1/organization_pb';
|
||||
import { OrganizationService } from '../services/organization.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav',
|
||||
@@ -21,18 +27,35 @@ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
MatSidenavModule,
|
||||
MatListModule,
|
||||
MatIconModule,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
RouterOutlet,
|
||||
MatCardModule,
|
||||
ProfileButtonComponent,
|
||||
]
|
||||
})
|
||||
export class NavComponent {
|
||||
export class NavComponent implements OnInit {
|
||||
private breakpointObserver = inject(BreakpointObserver);
|
||||
private userService = inject(UserService);
|
||||
private orgService = inject(OrganizationService);
|
||||
|
||||
claims$!: Observable<Claims | null>;
|
||||
org$!: Observable<Organization | undefined>;
|
||||
|
||||
refreshOrg(): void {
|
||||
this.orgService.refreshOrganizations()
|
||||
}
|
||||
|
||||
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
|
||||
.pipe(
|
||||
map(result => result.matches),
|
||||
shareReplay()
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.claims$ = this.userService.getClaims();
|
||||
this.org$ = this.orgService.activeOrg();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@if (claims$ | async; as claims) {
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
@if (claims.picture) {
|
||||
<img class="profile-picture" [src]="claims.picture" alt="Profile"/>
|
||||
} @else {
|
||||
<mat-icon>account_circle</mat-icon>
|
||||
}
|
||||
</button>
|
||||
|
||||
<mat-menu class="accounts-menu" #menu="matMenu">
|
||||
<mat-card class="accounts-card">
|
||||
<mat-card-header>
|
||||
<div mat-card-avatar class="accounts-header-image" [ngStyle]="{'background-image': claims.picture ? 'url(' + claims.picture +')' : 'url(/ui/assets/img/account_circle.svg)'}">
|
||||
</div>
|
||||
<mat-card-title>{{ claims.name }}</mat-card-title>
|
||||
<mat-card-subtitle>{{ claims.email }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-actions>
|
||||
<a mat-menu-item href="{{claims.iss}}/ui/console/users/me?id=general">
|
||||
Profile
|
||||
</a>
|
||||
<a mat-menu-item href="{{claims.iss}}/oidc/v1/end_session">
|
||||
Logout
|
||||
</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</mat-menu>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.profile-picture {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.accounts-card {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.accounts-header-image {
|
||||
background-image: url('/ui/assets/img/account_circle.svg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-menu-content {
|
||||
padding: 0px !important;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProfileButtonComponent } from './profile-button.component';
|
||||
|
||||
describe('ProfileButtonComponent', () => {
|
||||
let component: ProfileButtonComponent;
|
||||
let fixture: ComponentFixture<ProfileButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ProfileButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ProfileButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Claims } from '../gen/holos/v1alpha1/user_pb';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AsyncPipe, NgIf, NgStyle } from '@angular/common';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-button',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
AsyncPipe,
|
||||
NgIf,
|
||||
NgStyle,
|
||||
],
|
||||
templateUrl: './profile-button.component.html',
|
||||
styleUrl: './profile-button.component.scss'
|
||||
})
|
||||
export class ProfileButtonComponent {
|
||||
@Input({ required: true }) claims$!: Observable<Claims | null>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OrganizationService } from './organization.service';
|
||||
|
||||
describe('OrganizationService', () => {
|
||||
let service: OrganizationService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(OrganizationService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { OrganizationService as ConnectOrganizationService } from '../gen/holos/v1alpha1/organization_connect';
|
||||
import { ObservableClient } from '../../connect/observable-client';
|
||||
import { Observable, switchMap, of, shareReplay, catchError, BehaviorSubject } from 'rxjs';
|
||||
import { GetCallerOrganizationsResponse, Organization } from '../gen/holos/v1alpha1/organization_pb';
|
||||
import { UserService } from './user.service';
|
||||
import { Code, ConnectError } from '@connectrpc/connect';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OrganizationService {
|
||||
private callerOrganizationsTrigger$ = new BehaviorSubject<void>(undefined);
|
||||
private callerOrganizations$: Observable<GetCallerOrganizationsResponse>;
|
||||
|
||||
private fetchCallerOrganizations(): Observable<GetCallerOrganizationsResponse> {
|
||||
return this.client.getCallerOrganizations({ request: {} }).pipe(
|
||||
switchMap(resp => {
|
||||
if (resp && resp.organizations.length > 0) {
|
||||
return of(resp)
|
||||
}
|
||||
return this.client.createCallerOrganization({ request: {} })
|
||||
}),
|
||||
catchError(err => {
|
||||
if (err instanceof ConnectError) {
|
||||
if (err.code == Code.NotFound) {
|
||||
return this.userService.createUser().pipe(
|
||||
switchMap(user => this.client.createCallerOrganization({ request: {} }))
|
||||
)
|
||||
}
|
||||
}
|
||||
console.error('Error fetching data:', err);
|
||||
throw err;
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
getOrganizations(): Observable<Organization[]> {
|
||||
return this.callerOrganizations$.pipe(
|
||||
switchMap(resp => of(resp.organizations))
|
||||
)
|
||||
}
|
||||
|
||||
activeOrg(): Observable<Organization | undefined> {
|
||||
return this.callerOrganizations$.pipe(
|
||||
switchMap(resp => of(resp.organizations.at(-1)))
|
||||
)
|
||||
}
|
||||
|
||||
refreshOrganizations(): void {
|
||||
this.callerOrganizationsTrigger$.next()
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(ConnectOrganizationService) private client: ObservableClient<typeof ConnectOrganizationService>,
|
||||
private userService: UserService,
|
||||
) {
|
||||
this.callerOrganizations$ = this.callerOrganizationsTrigger$.pipe(
|
||||
switchMap(() => this.fetchCallerOrganizations()),
|
||||
shareReplay(1),
|
||||
catchError(err => {
|
||||
console.error('Error fetching data:', err);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UserService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
42
internal/frontend/holos/src/app/services/user.service.ts
Normal file
42
internal/frontend/holos/src/app/services/user.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Observable, switchMap, of, shareReplay } from 'rxjs';
|
||||
import { ObservableClient } from '../../connect/observable-client';
|
||||
import { Claims, User } from '../gen/holos/v1alpha1/user_pb';
|
||||
import { UserService as ConnectUserService } from '../gen/holos/v1alpha1/user_connect';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserService {
|
||||
|
||||
getClaims(): Observable<Claims | null> {
|
||||
return this.client.getCallerClaims({ request: {} }).pipe(
|
||||
switchMap(getCallerClaimsResponse => {
|
||||
if (getCallerClaimsResponse && getCallerClaimsResponse.claims) {
|
||||
return of(getCallerClaimsResponse.claims)
|
||||
} else {
|
||||
return of(null)
|
||||
}
|
||||
}),
|
||||
// Consolidate to one api call for all subscribers
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
createUser(): Observable<User | null> {
|
||||
return this.client.createCallerUser({ request: {} }).pipe(
|
||||
switchMap(resp => {
|
||||
if (resp && resp.user) {
|
||||
return of(resp.user)
|
||||
} else {
|
||||
return of(null)
|
||||
}
|
||||
}),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(ConnectUserService) private client: ObservableClient<typeof ConnectUserService>,
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
|
||||
|
After Width: | Height: | Size: 365 B |
15
internal/frontend/holos/src/connect/client.provider.ts
Normal file
15
internal/frontend/holos/src/connect/client.provider.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Provider } from "@angular/core";
|
||||
import { Transport } from "@connectrpc/connect";
|
||||
import { ServiceType } from "@bufbuild/protobuf";
|
||||
import { createObservableClient } from "./observable-client";
|
||||
import { TRANSPORT } from "./transport.token";
|
||||
|
||||
export function provideClient<T extends ServiceType>(service: T): Provider {
|
||||
return {
|
||||
provide: service,
|
||||
useFactory: (transport: Transport) => {
|
||||
return createObservableClient(service, transport);
|
||||
},
|
||||
deps: [TRANSPORT],
|
||||
};
|
||||
}
|
||||
30
internal/frontend/holos/src/connect/connect.module.ts
Normal file
30
internal/frontend/holos/src/connect/connect.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core'
|
||||
import { Interceptor } from '@connectrpc/connect'
|
||||
import { createConnectTransport } from '@connectrpc/connect-web'
|
||||
import { INTERCEPTORS } from './interceptor.token'
|
||||
import { TRANSPORT } from './transport.token'
|
||||
|
||||
@NgModule()
|
||||
export class ConnectModule {
|
||||
public static forRoot(
|
||||
connectOptions: Omit<
|
||||
Parameters<typeof createConnectTransport>[0],
|
||||
'interceptors'
|
||||
>
|
||||
): ModuleWithProviders<ConnectModule> {
|
||||
return {
|
||||
ngModule: ConnectModule,
|
||||
providers: [
|
||||
{
|
||||
provide: TRANSPORT,
|
||||
useFactory: (interceptors: Interceptor[]) =>
|
||||
createConnectTransport({
|
||||
...connectOptions,
|
||||
interceptors: interceptors,
|
||||
}),
|
||||
deps: [INTERCEPTORS],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
30
internal/frontend/holos/src/connect/grpc-web.module.ts
Normal file
30
internal/frontend/holos/src/connect/grpc-web.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core'
|
||||
import { Interceptor } from '@connectrpc/connect'
|
||||
import { createGrpcWebTransport } from '@connectrpc/connect-web'
|
||||
import { INTERCEPTORS } from './interceptor.token'
|
||||
import { TRANSPORT } from './transport.token'
|
||||
|
||||
@NgModule()
|
||||
export class GrpcWebModule {
|
||||
public static forRoot(
|
||||
grpcWebOptions: Omit<
|
||||
Parameters<typeof createGrpcWebTransport>[0],
|
||||
'interceptors'
|
||||
>
|
||||
): ModuleWithProviders<GrpcWebModule> {
|
||||
return {
|
||||
ngModule: GrpcWebModule,
|
||||
providers: [
|
||||
{
|
||||
provide: TRANSPORT,
|
||||
useFactory: (interceptors: Interceptor[]) =>
|
||||
createGrpcWebTransport({
|
||||
...grpcWebOptions,
|
||||
interceptors: interceptors,
|
||||
}),
|
||||
deps: [INTERCEPTORS],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
9
internal/frontend/holos/src/connect/interceptor.token.ts
Normal file
9
internal/frontend/holos/src/connect/interceptor.token.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { InjectionToken } from '@angular/core'
|
||||
import type { Interceptor } from '@connectrpc/connect'
|
||||
|
||||
export const INTERCEPTORS = new InjectionToken<Interceptor[]>(
|
||||
'connect.interceptors',
|
||||
{
|
||||
factory: () => [],
|
||||
}
|
||||
)
|
||||
120
internal/frontend/holos/src/connect/observable-client.ts
Normal file
120
internal/frontend/holos/src/connect/observable-client.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { makeAnyClient, CallOptions, Transport } from '@connectrpc/connect'
|
||||
import { createAsyncIterable } from '@connectrpc/connect/protocol'
|
||||
import {
|
||||
ServiceType,
|
||||
PartialMessage,
|
||||
MethodInfoServerStreaming,
|
||||
MethodInfo,
|
||||
MethodInfoUnary,
|
||||
MethodKind,
|
||||
Message,
|
||||
} from '@bufbuild/protobuf'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
export type ObservableClient<T extends ServiceType> = {
|
||||
[P in keyof T['methods']]: T['methods'][P] extends MethodInfoUnary<
|
||||
infer I,
|
||||
infer O
|
||||
>
|
||||
? UnaryFn<I, O>
|
||||
: T['methods'][P] extends MethodInfoServerStreaming<infer I, infer O>
|
||||
? ServerStreamingFn<I, O>
|
||||
: never
|
||||
}
|
||||
|
||||
export function createObservableClient<T extends ServiceType>(
|
||||
service: T,
|
||||
transport: Transport
|
||||
) {
|
||||
return makeAnyClient(service, (method) => {
|
||||
switch (method.kind) {
|
||||
case MethodKind.Unary:
|
||||
return createUnaryFn(transport, service, method)
|
||||
case MethodKind.ServerStreaming:
|
||||
return createServerStreamingFn(transport, service, method)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}) as ObservableClient<T>
|
||||
}
|
||||
|
||||
type UnaryFn<I extends Message<I>, O extends Message<O>> = (
|
||||
request: PartialMessage<I>,
|
||||
options?: CallOptions
|
||||
) => Observable<O>
|
||||
|
||||
function createUnaryFn<I extends Message<I>, O extends Message<O>>(
|
||||
transport: Transport,
|
||||
service: ServiceType,
|
||||
method: MethodInfo<I, O>
|
||||
): UnaryFn<I, O> {
|
||||
return function (requestMessage, options) {
|
||||
return new Observable<O>((subscriber) => {
|
||||
transport
|
||||
.unary(
|
||||
service,
|
||||
method,
|
||||
options?.signal,
|
||||
options?.timeoutMs,
|
||||
options?.headers,
|
||||
requestMessage
|
||||
)
|
||||
.then(
|
||||
(response) => {
|
||||
options?.onHeader?.(response.header)
|
||||
subscriber.next(response.message)
|
||||
options?.onTrailer?.(response.trailer)
|
||||
},
|
||||
(err) => {
|
||||
subscriber.error(err)
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
subscriber.complete()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type ServerStreamingFn<I extends Message<I>, O extends Message<O>> = (
|
||||
request: PartialMessage<I>,
|
||||
options?: CallOptions
|
||||
) => Observable<O>
|
||||
|
||||
export function createServerStreamingFn<
|
||||
I extends Message<I>,
|
||||
O extends Message<O>
|
||||
>(
|
||||
transport: Transport,
|
||||
service: ServiceType,
|
||||
method: MethodInfo<I, O>
|
||||
): ServerStreamingFn<I, O> {
|
||||
return function (input, options) {
|
||||
return new Observable<O>((subscriber) => {
|
||||
transport
|
||||
.stream<I, O>(
|
||||
service,
|
||||
method,
|
||||
options?.signal,
|
||||
options?.timeoutMs,
|
||||
options?.headers,
|
||||
createAsyncIterable([input])
|
||||
)
|
||||
.then(
|
||||
async (streamResponse) => {
|
||||
options?.onHeader?.(streamResponse.header)
|
||||
for await (const response of streamResponse.message) {
|
||||
subscriber.next(response)
|
||||
}
|
||||
options?.onTrailer?.(streamResponse.trailer)
|
||||
},
|
||||
(err) => {
|
||||
subscriber.error(err)
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
subscriber.complete()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
4
internal/frontend/holos/src/connect/transport.token.ts
Normal file
4
internal/frontend/holos/src/connect/transport.token.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from "@angular/core";
|
||||
import type { Transport } from "@connectrpc/connect";
|
||||
|
||||
export const TRANSPORT = new InjectionToken<Transport>("connect.transport");
|
||||
@@ -3,7 +3,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
"types": [],
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
|
||||
@@ -8,12 +8,10 @@ import (
|
||||
"unicode"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/gofrs/uuid"
|
||||
"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/server/middleware/authn"
|
||||
"github.com/holos-run/holos/internal/server/middleware/logger"
|
||||
holos "github.com/holos-run/holos/service/gen/holos/v1alpha1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
@@ -67,7 +65,7 @@ func (h *OrganizationHandler) GetCallerOrganizations(
|
||||
func (h *OrganizationHandler) CreateCallerOrganization(
|
||||
ctx context.Context,
|
||||
req *connect.Request[holos.CreateCallerOrganizationRequest],
|
||||
) (*connect.Response[holos.CreateCallerOrganizationResponse], error) {
|
||||
) (*connect.Response[holos.GetCallerOrganizationsResponse], error) {
|
||||
authnID, err := authn.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
|
||||
@@ -86,7 +84,7 @@ func (h *OrganizationHandler) CreateCallerOrganization(
|
||||
err = WithTx(ctx, h.db, func(tx *ent.Tx) (err error) {
|
||||
org, err = h.db.Organization.Create().
|
||||
SetName(cleanAndAppendRandom(authnID.Name())).
|
||||
SetDisplayName(authnID.Name() + "'s Org").
|
||||
SetDisplayName(authnID.GivenName() + "'s Org").
|
||||
SetCreatorID(dbUser.ID).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
@@ -111,7 +109,7 @@ func (h *OrganizationHandler) CreateCallerOrganization(
|
||||
rpcOrgs = append(rpcOrgs, OrganizationToRPC(dbOrg))
|
||||
}
|
||||
|
||||
res := connect.NewResponse(&holos.CreateCallerOrganizationResponse{
|
||||
res := connect.NewResponse(&holos.GetCallerOrganizationsResponse{
|
||||
User: UserToRPC(dbUser),
|
||||
Organizations: rpcOrgs,
|
||||
})
|
||||
@@ -126,7 +124,7 @@ func cleanAndAppendRandom(s string) string {
|
||||
return -1
|
||||
}
|
||||
cleaned := strings.Map(mapping, s)
|
||||
randNum := rand.Intn(1_000_000)
|
||||
randNum := rand.Intn(900_000) + 100_000
|
||||
return fmt.Sprintf("%s-%06d", cleaned, randNum)
|
||||
}
|
||||
|
||||
@@ -143,24 +141,3 @@ func OrganizationToRPC(org *ent.Organization) *holos.Organization {
|
||||
}
|
||||
return &rpcEntity
|
||||
}
|
||||
|
||||
func createOrganization(ctx context.Context, client *ent.Client, name string, displayName string, creatorID uuid.UUID) (*ent.Organization, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
// Create the user, error if it already exists
|
||||
entity, err := client.Organization.
|
||||
Create().
|
||||
SetName(name).
|
||||
SetDisplayName(displayName).
|
||||
SetCreatorID(creatorID).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
err = connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
|
||||
log.ErrorContext(ctx, "could not create user", "err", err)
|
||||
return entity, err
|
||||
}
|
||||
|
||||
log = log.With("organization", entity)
|
||||
log.InfoContext(ctx, "created")
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ func (h *UserHandler) GetCallerClaims(
|
||||
EmailVerified: authnID.Verified(),
|
||||
Name: authnID.Name(),
|
||||
Groups: authnID.Groups(),
|
||||
GivenName: authnID.GivenName(),
|
||||
FamilyName: authnID.FamilyName(),
|
||||
Picture: authnID.Picture(),
|
||||
},
|
||||
})
|
||||
return res, nil
|
||||
@@ -135,20 +138,3 @@ func createUser(ctx context.Context, client *ent.Client, name string, claims aut
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func getAuthenticatedUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
|
||||
authnIdentity, err := authn.FromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
|
||||
}
|
||||
log := logger.FromContext(ctx).With("iss", authnIdentity.Issuer(), "sub", authnIdentity.Subject(), "email", authnIdentity.Email())
|
||||
user, err := client.User.Query().Where(
|
||||
user.Iss(authnIdentity.Issuer()),
|
||||
user.Sub(authnIdentity.Subject()),
|
||||
).Only(ctx)
|
||||
if err != nil {
|
||||
log.DebugContext(ctx, "could not get user", "err", err)
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@ type Identity interface {
|
||||
Name() string
|
||||
// Groups is the groups claim.
|
||||
Groups() []string
|
||||
// GivenName is the given name of the user.
|
||||
GivenName() string
|
||||
// FamilyName is the family name of the user.
|
||||
FamilyName() string
|
||||
// Picture is an optional avatar image url for the user.
|
||||
Picture() string
|
||||
}
|
||||
|
||||
// key is an unexported type for keys defined in this package to prevent
|
||||
@@ -104,12 +110,15 @@ func NewVerifier(ctx context.Context, log *slog.Logger, issuer string) (*oidc.ID
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
Groups []string `json:"groups"`
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
Groups []string `json:"groups"`
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
Picture string `json:"picture"`
|
||||
}
|
||||
|
||||
type user struct {
|
||||
@@ -136,6 +145,18 @@ func (u user) Groups() []string {
|
||||
return u.claims.Groups
|
||||
}
|
||||
|
||||
func (u user) GivenName() string {
|
||||
return u.claims.GivenName
|
||||
}
|
||||
|
||||
func (u user) FamilyName() string {
|
||||
return u.claims.FamilyName
|
||||
}
|
||||
|
||||
func (u user) Picture() string {
|
||||
return u.claims.Picture
|
||||
}
|
||||
|
||||
func (u user) Verified() bool {
|
||||
return u.claims.Verified
|
||||
}
|
||||
|
||||
@@ -28,12 +28,7 @@ message GetCallerOrganizationsResponse {
|
||||
|
||||
message CreateCallerOrganizationRequest {}
|
||||
|
||||
message CreateCallerOrganizationResponse {
|
||||
User user = 1;
|
||||
repeated Organization organizations = 2;
|
||||
}
|
||||
|
||||
service OrganizationService {
|
||||
rpc GetCallerOrganizations(GetCallerOrganizationsRequest) returns (GetCallerOrganizationsResponse) {}
|
||||
rpc CreateCallerOrganization(CreateCallerOrganizationRequest) returns (CreateCallerOrganizationResponse) {}
|
||||
rpc CreateCallerOrganization(CreateCallerOrganizationRequest) returns (GetCallerOrganizationsResponse) {}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ message Claims {
|
||||
bool email_verified = 4;
|
||||
string name = 5 [(buf.validate.field).string.max_len = 100];
|
||||
repeated string groups = 6;
|
||||
string given_name = 7;
|
||||
string family_name = 8;
|
||||
string picture = 9;
|
||||
}
|
||||
|
||||
// UserClaims represents id token claims
|
||||
|
||||
@@ -1 +1 @@
|
||||
69
|
||||
70
|
||||
|
||||
Reference in New Issue
Block a user