diff --git a/assets/main_background.jpg b/assets/main_background.jpg new file mode 100644 index 0000000..8d0f09f Binary files /dev/null and b/assets/main_background.jpg differ diff --git a/lib/api/grpc/environments.dart b/lib/api/grpc/environments.dart index 2846a14..ff91708 100644 --- a/lib/api/grpc/environments.dart +++ b/lib/api/grpc/environments.dart @@ -2,13 +2,18 @@ import 'package:softplayer_dart_proto/main.dart'; import 'package:softplayer_web/api/grpc/creds.dart'; class EnvironmentLocalData { - EnvironmentLocalData({ - required this.uuid, - required this.token, - }); + EnvironmentLocalData( + {required this.serverType, + required this.serverLocation, + required this.provider, + required this.name, + required this.description}); - String uuid; - String token; + final String name; + final String description; + final String provider; + final String serverType; + final String serverLocation; } class EnvironmentsGrpc { @@ -19,23 +24,62 @@ class EnvironmentsGrpc { EnvironmentsGrpc(channel) : envStub = EnvironmentsClient(channel); // Get environments from the API - Future get(String name, SoftplayerCreds creds) async { + Future get(String name, SoftplayerCreds creds) async { final request = GetOptions( - name: EnvironmentName(name: name), + metadata: EnvironmentMetadata(name: name), ownerId: OwnerId(uuid: creds.uuid), token: Token(token: creds.token), ); try { final response = await envStub.get(request); - return response; + return EnvironmentLocalData( + serverType: response.spec.serverType.toString(), + serverLocation: response.spec.serverLocation.toString(), + provider: response.spec.provider.toString(), + name: response.metadata.name, + description: response.metadata.description); } catch (e) { rethrow; } } - Stream> list(SoftplayerCreds creds) async* { - List envs = []; + Future create( + EnvironmentLocalData data, SoftplayerCreds creds) async { + Location.values.forEach((element) => print(element.toString() + " - " + data.serverLocation)); + print(ServerType.values); + + final request = CreateOptions( + metadata: + EnvironmentMetadata(description: data.description, name: data.name), + spec: EnvironmentSpec( + // Currently we do not support other kinds + kubernetes: Kubernetes.KUBERNETES_K3S, + // Currently we do not support other providers + provider: Provider.PROVIDER_HETZNER, + serverLocation: Location.values + .firstWhere((e) => e.toString() == data.serverLocation), + serverType: ServerType.values + .firstWhere((e) => e.toString() == data.serverType), + ), + ownerId: OwnerId(uuid: creds.uuid), + token: Token(token: creds.token), + ); + try { + final response = await envStub.create(request); + return EnvironmentLocalData( + serverType: response.spec.serverType.toString(), + serverLocation: response.spec.serverLocation.toString(), + provider: response.spec.provider.toString(), + name: response.metadata.name, + description: response.metadata.description); + } catch (e) { + rethrow; + } + } + + Stream> list(SoftplayerCreds creds) async* { + List envs = []; try { await for (var feature in envStub.list( ListOptions( @@ -43,9 +87,12 @@ class EnvironmentsGrpc { token: Token(token: creds.token), ), )) { - envs.add(EnvironmentFull( - data: feature.data, - name: feature.name, + envs.add(EnvironmentLocalData( + serverType: feature.spec.serverType.toString(), + serverLocation: feature.spec.serverLocation.toString(), + provider: feature.spec.provider.toString(), + name: feature.metadata.name, + description: feature.metadata.description, )); } } catch (e) { diff --git a/lib/components/create_env_form.dart b/lib/components/create_env_form.dart new file mode 100644 index 0000000..f9b5c26 --- /dev/null +++ b/lib/components/create_env_form.dart @@ -0,0 +1,131 @@ +import 'dart:html'; +import 'dart:js' as js; +import 'package:flutter/material.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_web/api/grpc/creds.dart'; +import 'package:softplayer_web/api/grpc/environments.dart'; + +class CreateEnvForm extends StatefulWidget { + CreateEnvForm(GrpcWebClientChannel channel, {super.key}) + : environmentsGrpc = EnvironmentsGrpc(channel); + final EnvironmentsGrpc environmentsGrpc; + @override + State createState() => _CreateEnvFormState(); +} + +class _CreateEnvFormState extends State { + final _formKey = GlobalKey(); + + final nameCtl = TextEditingController(); + final descriptionCtl = TextEditingController(); + String? serverLocation; + String? serverType; + + void createEnvironment() { + // Validate returns true if the form is valid, or false otherwise. + if (_formKey.currentState!.validate()) { + final name = nameCtl.text; + final description = descriptionCtl.text; + widget.environmentsGrpc + .create( + EnvironmentLocalData( + serverType: serverType!, + serverLocation: serverLocation!, + provider: "", + name: name, + description: description), + SoftplayerCredsHelpers().fromLocalStorage()) + .then((rs) { + Navigator.pop(context); + }).catchError((e) { + print(e); + GrpcError error = e; + String msg; + if (error.message != null) { + msg = error.message!; + } else { + msg = error.toString(); + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg), + backgroundColor: Colors.red, + showCloseIcon: true, + behavior: SnackBarBehavior.floating, + )); + }); + } + } + + Widget createEnvForm() => SizedBox( + width: 420, + child: Form( + key: _formKey, + child: Center( + child: Column(children: [ + Divider(), + Text("Provider the environment metadata"), + TextFormField( + autofocus: true, + controller: nameCtl, + decoration: const InputDecoration( + hintText: "Enter the environment name", + icon: Icon(Icons.computer), + label: Text("Name"), + ), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + TextFormField( + autofocus: true, + controller: descriptionCtl, + decoration: const InputDecoration( + hintText: "Enter the environment description", + icon: Icon(Icons.description), + label: Text("Description"), + ), + maxLength: 360, + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + Divider(), + DropdownButtonFormField( + decoration: const InputDecoration( + hintText: "Enter the environment description", + icon: Icon(Icons.computer), + label: Text("Type of the server"), + ), + value: null, + isDense: true, + items: [ + DropdownMenuItem( + child: Text("lala1"), + value: "test1", + ), + DropdownMenuItem(child: Text("lala2"), value: "test2"), + DropdownMenuItem(child: Text("lala3"), value: "test3"), + DropdownMenuItem(child: Text("lala4"), value: "test4"), + ], + onChanged: (value) => print(value), + ), + TextButton( + onPressed: () => js.context.callMethod( + 'open', ['https://stackoverflow.com/questions/ask']), + child: Text("Read more about environment types here"), + ) + ])))); + List createEnvActions() => [ + TextButton( + onPressed: createEnvironment, + child: const Text('OK'), + ), + ]; + + @override + Widget build(BuildContext context) => AlertDialog( + title: const Text("Create a new environment"), + content: createEnvForm(), + actions: createEnvActions(), + ); +} diff --git a/lib/components/environment_card.dart b/lib/components/environment_card.dart index 363605f..49c41f3 100644 --- a/lib/components/environment_card.dart +++ b/lib/components/environment_card.dart @@ -1,23 +1,50 @@ import 'package:flutter/material.dart'; +import 'package:softplayer_web/api/grpc/environments.dart'; class EnvirnomentCard extends StatelessWidget { - final String name; + final EnvironmentLocalData env; const EnvirnomentCard({ super.key, - required this.name, + required this.env, }); @override Widget build(BuildContext context) { return Card( - child: Column( - children: [ - Text(name), - Row( - children: [Text(name)], - ) - ], + child: SelectionArea( + child: Column( + children: [ + Text(env.name), + const Divider(), + Table( + border: const TableBorder( + bottom: BorderSide.none, + left: BorderSide.none, + right: BorderSide.none, + top: BorderSide.none, + ), + children: [ + TableRow(children: [ + Text("Provider"), + Text(env.provider), + ]), + TableRow(children: [ + Text("Description"), + Text(env.description), + ]), + TableRow(children: [ + Text("Server Type"), + Text(env.serverType), + ]), + TableRow(children: [ + Text("Location"), + Text(env.serverLocation), + ]), + ], + ) + ], + ), ), ); } diff --git a/lib/components/environments.dart b/lib/components/environments.dart index 9b8a890..60bcb02 100644 --- a/lib/components/environments.dart +++ b/lib/components/environments.dart @@ -26,7 +26,15 @@ class _EnvirnomentListState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/login_background.jpg"), + fit: BoxFit.cover, + ), + ), child: StreamBuilder( stream: envGrpc.list(SoftplayerCredsHelpers().fromLocalStorage()), @@ -37,66 +45,37 @@ class _EnvirnomentListState extends State { if (snapshot.hasError) { return const Text('Error!'); } else { - return GridView.count( - crossAxisCount: 4, - children: snapshot.data! - .map((e) => EnvirnomentCard(name: e.name.name)) - .toList(), - ); + if (snapshot.hasData) { + var data = snapshot.requireData; + if (data.isNotEmpty) { + return GridView.count( + crossAxisCount: 4, + children: snapshot.data! + .map((e) => EnvirnomentCard( + env: e, + )) + .toList(), + ); + } else { + print("npo data"); + return Center( + child: Container( + height: 300, + child: Text( + "To get strated, use the button in the corner"), + decoration: BoxDecoration( + border: Border.all(), + shape: BoxShape.rectangle, + borderRadius: + BorderRadius.all(Radius.circular(10)), + color: Color.fromRGBO(100, 150, 80, 20), + ), + )); + } + } } } return const Text("err"); }))); } } - -//class EnvirnomentList extends StatelessWidget { -// EnvirnomentList({ -// super.key, -// required this.channel, -// }); -// final GrpcWebClientChannel channel; -// late List envs; -// late EnvironmentsGrpc envGrpc; -// List getEnvs() { -// List envs = []; -// envGrpc.list().then((value) { -// return value; -// }).catchError((e) { -// return envs; -// }); -// return envs; -// } -// -// List bootstrapCards(List envs) { -// List cards = []; -// envs.forEach((element) { -// cards.add(Center(child: Text(element))); -// }); -// return cards; -// } -// -// -// @override -// Widget build(BuildContext context) { -// envGrpc = EnvironmentsGrpc(channel: channel); -// envGrpc.init(); -// envs = getEnvs(); -// return GridView.count( -// crossAxisCount: 2, -// children: bootstrapCards(envs), -// ); -// // children: List.generate(100, (index) { -// // return Center( -// // child: Text( -// // 'Item $index', -// // style: Theme.of(context).textTheme.headlineSmall, -// // ), -// // ); -// // }), -// // ); -// // return GridView.count( -// // children: bootstrapCards(getEnvs()), -// // ); -// } -//} diff --git a/lib/helpers/providers/common.dart b/lib/helpers/providers/common.dart new file mode 100644 index 0000000..0fc9805 --- /dev/null +++ b/lib/helpers/providers/common.dart @@ -0,0 +1,4 @@ +abstract class Provider { + String getServerType(String serverType); + String rawServerType(String serverType); +} \ No newline at end of file diff --git a/lib/helpers/providers/hetzner.dart b/lib/helpers/providers/hetzner.dart new file mode 100644 index 0000000..9dc9c31 --- /dev/null +++ b/lib/helpers/providers/hetzner.dart @@ -0,0 +1,26 @@ +import 'package:softplayer_web/helpers/providers/common.dart'; + +class Hetzner implements Provider { + @override + String getServerType(String serverType) { + switch (serverType) { + case "SERVER_TYPE_STARTER": + return "starter"; + case "SERVER_TYPE_S": + return "starter"; + case "SERVER_TYPE_STARTER": + return "starter"; + case "SERVER_TYPE_STARTER": + return "starter"; + + default: + } + throw UnimplementedError(); + } + + @override + String rawServerType(string) { + // TODO: implement rawServerType + throw UnimplementedError(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 0cb0bea..845be2a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'dart:html'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:grpc/grpc_web.dart'; import 'package:softplayer_web/api/grpc/accounts.dart'; +import 'package:softplayer_web/components/create_env_form.dart'; import 'package:softplayer_web/components/environments.dart'; import 'package:softplayer_web/components/login_form.dart'; @@ -61,10 +63,12 @@ class _StateRootWidget extends State { if (!isSignedIn()) { return Scaffold( body: Container( + width: double.infinity, + height: double.infinity, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage("assets/login_background.jpg"), - fit: BoxFit.fill, + fit: BoxFit.cover, ), ), child: LoginForm( @@ -73,8 +77,9 @@ class _StateRootWidget extends State { ), )); } else { + EnvirnomentList envList = EnvirnomentList(channel: widget.channel); return Scaffold( - body: EnvirnomentList(channel: widget.channel), + body: envList, appBar: AppBar( title: const Text("Softplayer"), actions: [ @@ -88,7 +93,12 @@ class _StateRootWidget extends State { ], ), floatingActionButton: FloatingActionButton( - onPressed: () => print("1"), + child: const Icon(Icons.add), + onPressed: () => showDialog( + context: context, + builder: (context) => + CreateEnvForm(widget.channel), + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index aa17454..b16bbdb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -245,7 +245,7 @@ packages: description: path: "." ref: main - resolved-ref: "6b6325ac4b6c469e41c994d1045af79225a49d38" + resolved-ref: bccaaff90c8ef1e9d23c565a58cab6db841282d3 url: "https://git.badhouseplants.net/softplayer/softplayer-dart-proto.git" source: git version: "1.0.0+1"