Move UI out of experimental mode (#7012)

* Make UI not experimental

* Display API and UI URLs

* Remove link to old sources

* Fix integration tests

* Add UI to Usage Data

* Add a "Using the GUI to edit the Devfile" page to doc

* Add link to odo.dev specific page

* Apply suggestions from code review

Co-authored-by: Armel Soro <armel@rm3l.org>

* Change favicon with odo logo

* Display web console URL as part of the Dev status

* Update UI static files

* Document that Comments not supported

* Add UI screenshots

---------

Co-authored-by: Armel Soro <armel@rm3l.org>
This commit is contained in:
Philippe Martin
2023-08-04 13:02:34 +02:00
committed by GitHub
parent 7c976f1bf1
commit b93a75c11a
24 changed files with 173 additions and 87 deletions

View File

@@ -30,6 +30,20 @@ The following tables describe the additional information collected by `odo` comm
| odo deploy | Component Type, Devfile Name, Language, Project Type, Platform (kubernetes, openshift), Platform version |
| odo <create/set/delete> namespace | Cluster Type (Possible values: OpenShift 3, OpenShift 4, Kubernetes) |
**odo v3 GUI**
The odo v3 GUI is accessible (by default at http://localhost:20000) when the command `odo dev` is running.
| Page | Data
|----------------------|-------------------------
| YAML (main page) | Page accessed, UI started, Devfile saved to disk, Devfile cleared, Devfile applied |
| Metadata | Page accessed, Metadata applied |
| Commands | Page accessed, Start create command, Create command |
| Events | Page accessed, Add event |
| Containers | Page accessed, Create container |
| Images | Page accessed, Create Image |
| Resources | Page accessed, Create Resource |
**odo v2**
| Command | Data |

View File

@@ -0,0 +1,74 @@
---
title: Using the Web UI to edit the Devfile
sidebar_position: 3
---
When the command `odo dev` is running, a Graphical User Interface (GUI) is accessible (generally at http://localhost:20000).
The interface can be used to edit the Devfile used by the `odo dev` session.
The interface proposes three representations of the Devfile: a textual representation in YAML format, a Chart, and a graphical representation.
You can edit any representation independently, the other representations will be modified accordingly. The chart is read-only, the two other representations can be freely edited.
The YAML representation reflects exactly the content of the `devfile.yaml` file found into the directory where the `odo dev` session is running.
<figure>
<img
src={require('/static/img/ui-textual-representation.png').default}
alt="Textual Representation of a Devfile in Devfile Builder"
/>
<figcaption>A Textual Representation of a Devfile</figcaption>
</figure>
The page *Chart* contains a chart describing the different steps of the `odo dev` session.
<figure>
<img
src={require('/static/img/ui-chart-representation.png').default}
alt="Chart Representation of a Devfile in Devfile Builder"
/>
<figcaption>A Chart Representation of a Devfile</figcaption>
</figure>
The following pages of the UI contain a graphical representation of the Devfile. From these pages, you can edit the Devfile by adding and deleting objects (commands, events, containers, images, resources and volumes). From the *Commands* page, it is possible to change the *Kind* (Build, Run, Test, Debug or Deploy) of each command.
<figure>
<img
src={require('/static/img/ui-graphical-representation.png').default}
alt="Graphical Representation of a Devfile in Devfile Builder"
/>
<figcaption>A Graphical Representation of a Devfile</figcaption>
</figure>
When you *Save* the Devfile, the content of the YAML representation is saved to the disk, replacing the previous version of the Devfile. The `odo dev` session will react accordingly, depending on the changes done into the Devfile.
When the file `devfile.yaml` is modified, its content is sent to the GUI, which will alert you and give you the opportunity to accept the changes done into the file. If you accept, the changes you may have done in the interface will be lost.
## Limitations
### Limited Devfile Schema Versions
The only supported Devfile Schema version is 2.2.0.
### Limited support of parameters during object creation
When you create an object (either a command, a container or an image) from the graphical representation, you can fill in a limited number of parameters for the object, as only the more common parameters are presented in the creation form.
You can add parameters to the object after it has been created by editing it from the YAML representation.
### Limited support for parent devfile
When the current Devfile is referencing a parent Devfile (using the `.parent` field into the YAML), this parent is not represented into the GUI.
It is still possible to add a parent information to the YAML representation. It will be taken into account by the `odo dev` session once saved.
### Experimental Chart representation
The chart representation is experimental. It is possible that, for some complex Devfile, the chart is not accurate or is not displayed.
### Comments in YAML are not supported
Comments are generally supported in YAML. In the case of a Devfile, the library used to validate the contents of a Devfile discards these comments.
This results in that comments are removed from the YAML when you save it.

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -12,7 +12,9 @@ import (
openapi "github.com/redhat-developer/odo/pkg/apiserver-gen/go"
"github.com/redhat-developer/odo/pkg/apiserver-impl/sse"
"github.com/redhat-developer/odo/pkg/informer"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/cli/feature"
"github.com/redhat-developer/odo/pkg/podman"
"github.com/redhat-developer/odo/pkg/preference"
@@ -45,6 +47,7 @@ func StartServer(
podmanClient podman.Client,
stateClient state.Client,
preferenceClient preference.Client,
informerClient *informer.InformerClient,
) (ApiServer, error) {
pushWatcher := make(chan struct{})
defaultApiService := NewDefaultApiService(
@@ -129,7 +132,13 @@ func StartServer(
cancelFunc()
}
klog.V(0).Infof("API Server started at localhost:%d/api/v1", listeningPort)
if feature.IsEnabled(ctx, feature.UIServer) {
info := fmt.Sprintf("Web console accessible at http://localhost:%d/", listeningPort)
log.Spinner(info).End(true)
informerClient.AppendInfo(info + "\n")
}
log.Spinner(fmt.Sprintf("API Server started at http://localhost:%d/api/v1", listeningPort)).End(true)
log.Spinner(fmt.Sprintf("API documentation accessible at http://localhost:%d/swagger-ui/", listeningPort)).End(true)
go func() {
select {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -11,6 +11,6 @@
<body class="mat-typography">
<div id="loading">Loading, please wait...</div>
<app-root></app-root>
<script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.1f6f2714e5bbe400.js" type="module"></script>
<script src="runtime.1289ea0acffcdc5e.js" type="module"></script><script src="polyfills.8b3b37cedaf377c3.js" type="module"></script><script src="main.6a89d63885b00552.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -24,13 +24,6 @@ import (
"k8s.io/klog"
)
const (
promptMessage = `
[Ctrl+c] - Exit and delete resources from the cluster
[p] - Manually apply local changes to the application on the cluster
`
)
type DevClient struct {
kubernetesClient kclient.ClientInterface
prefClient preference.Client
@@ -97,7 +90,6 @@ func (o *DevClient) Start(
StartOptions: options,
DevfileWatchHandler: o.regenerateAdapterAndPush,
WatchCluster: true,
PromptMessage: promptMessage,
}
return o.watchClient.WatchAndPush(ctx, watchParameters, componentStatus)

View File

@@ -27,13 +27,6 @@ import (
corev1 "k8s.io/api/core/v1"
)
const (
promptMessage = `
[Ctrl+c] - Exit and delete resources from podman
[p] - Manually apply local changes to the application on podman
`
)
type DevClient struct {
fs filesystem.Filesystem
@@ -91,7 +84,6 @@ func (o *DevClient) Start(
StartOptions: options,
DevfileWatchHandler: o.watchHandler,
WatchCluster: false,
PromptMessage: promptMessage,
}
return o.watchClient.WatchAndPush(ctx, watchParameters, componentStatus)

3
pkg/informer/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// package informer provides a service to register information
// to display during Dev mode to the user in a regular basis
package informer

20
pkg/informer/informer.go Normal file
View File

@@ -0,0 +1,20 @@
package informer
type InformerClient struct {
info string
}
func NewInformerClient() *InformerClient {
return &InformerClient{}
}
func (o *InformerClient) AppendInfo(s string) {
if o.info != "" {
o.info += "\n"
}
o.info += s
}
func (o *InformerClient) GetInfo() string {
return o.info
}

View File

@@ -83,6 +83,7 @@ func (o *ApiServerOptions) Run(ctx context.Context) (err error) {
nil,
o.clientset.StateClient,
o.clientset.PreferenceClient,
o.clientset.InformerClient,
)
if err != nil {
return err
@@ -119,6 +120,7 @@ func NewCmdApiServer(ctx context.Context, name, fullName string, testClientset c
}
clientset.Add(apiserverCmd,
clientset.FILESYSTEM,
clientset.INFORMER,
clientset.STATE,
clientset.PREFERENCE,
)

View File

@@ -203,10 +203,10 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
componentName = odocontext.GetComponentName(ctx)
variables = fcontext.GetVariables(ctx)
platform = fcontext.GetPlatform(ctx, commonflags.PlatformCluster)
dest string
deployingTo string
)
var dest string
var deployingTo string
switch platform {
case commonflags.PlatformPodman:
dest = "Platform: podman"
@@ -286,6 +286,7 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
o.clientset.PodmanClient,
o.clientset.StateClient,
o.clientset.PreferenceClient,
o.clientset.InformerClient,
)
if err != nil {
return err
@@ -298,6 +299,10 @@ func (o *DevOptions) Run(ctx context.Context) (err error) {
}()
}
o.clientset.InformerClient.AppendInfo(log.Sbold("Keyboard Commands:") + "\n" +
"[Ctrl+c] - Exit and delete resources from " + deployingTo + "\n" +
" [p] - Manually apply local changes to the application on " + deployingTo + "\n")
return o.clientset.DevClient.Start(
o.ctx,
dev.StartOptions{
@@ -426,6 +431,7 @@ It forwards endpoints with any exposure values ('public', 'internal' or 'none')
clientset.DEV,
clientset.EXEC,
clientset.FILESYSTEM,
clientset.INFORMER,
clientset.INIT,
clientset.KUBERNETES_NULLABLE,
clientset.LOGS,

View File

@@ -19,7 +19,7 @@ var (
}
UIServer = OdoFeature{
isExperimental: true,
isExperimental: false,
}
)

View File

@@ -21,6 +21,7 @@ import (
"github.com/redhat-developer/odo/pkg/dev/kubedev"
"github.com/redhat-developer/odo/pkg/dev/podmandev"
"github.com/redhat-developer/odo/pkg/exec"
"github.com/redhat-developer/odo/pkg/informer"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/logs"
"github.com/redhat-developer/odo/pkg/odo/commonflags"
@@ -63,6 +64,8 @@ const (
EXEC = "DEP_EXEC"
// FILESYSTEM instantiates client for pkg/testingutil/filesystem
FILESYSTEM = "DEP_FILESYSTEM"
// INFORMER instantiates client for pkg/informer
INFORMER = "DEP_INFORMER"
// INIT instantiates client for pkg/init
INIT = "DEP_INIT"
// KUBERNETES_NULLABLE instantiates client for pkg/kclient, can be nil
@@ -121,7 +124,7 @@ var subdeps map[string][]string = map[string][]string{
REGISTRY: {FILESYSTEM, PREFERENCE, KUBERNETES_NULLABLE},
STATE: {FILESYSTEM},
SYNC: {EXEC},
WATCH: {KUBERNETES_NULLABLE},
WATCH: {INFORMER, KUBERNETES_NULLABLE},
BINDING: {PROJECT, KUBERNETES_NULLABLE},
/* Add sub-dependencies here, if any */
}
@@ -138,6 +141,7 @@ type Clientset struct {
DevClient dev.Client
ExecClient exec.Client
FS filesystem.Filesystem
InformerClient *informer.InformerClient
InitClient _init.Client
KubernetesClient kclient.ClientInterface
LogsClient logs.Client
@@ -197,6 +201,9 @@ func Fetch(command *cobra.Command, platform string, testClientset Clientset) (*C
dep.FS = filesystem.DefaultFs{}
}
}
if isDefined(command, INFORMER) {
dep.InformerClient = informer.NewInformerClient()
}
if isDefined(command, KUBERNETES) || isDefined(command, KUBERNETES_NULLABLE) {
if testClientset.KubernetesClient != nil {
dep.KubernetesClient = testClientset.KubernetesClient
@@ -293,7 +300,7 @@ func Fetch(command *cobra.Command, platform string, testClientset Clientset) (*C
}
}
if isDefined(command, WATCH) {
dep.WatchClient = watch.NewWatchClient(dep.KubernetesClient)
dep.WatchClient = watch.NewWatchClient(dep.KubernetesClient, dep.InformerClient)
}
if isDefined(command, BINDING) {
dep.BindingClient = binding.NewBindingClient(dep.ProjectClient, dep.KubernetesClient)

View File

@@ -12,6 +12,7 @@ import (
"github.com/redhat-developer/odo/pkg/dev"
"github.com/redhat-developer/odo/pkg/dev/common"
"github.com/redhat-developer/odo/pkg/informer"
"github.com/redhat-developer/odo/pkg/kclient"
"github.com/redhat-developer/odo/pkg/labels"
@@ -37,6 +38,7 @@ const (
type WatchClient struct {
kubeClient kclient.ClientInterface
informerClient *informer.InformerClient
sourcesWatcher *fsnotify.Watcher
deploymentWatcher watch.Interface
@@ -55,9 +57,13 @@ type WatchClient struct {
var _ Client = (*WatchClient)(nil)
func NewWatchClient(kubeClient kclient.ClientInterface) *WatchClient {
func NewWatchClient(
kubeClient kclient.ClientInterface,
informerClient *informer.InformerClient,
) *WatchClient {
return &WatchClient{
kubeClient: kubeClient,
informerClient: informerClient,
}
}
@@ -76,8 +82,6 @@ type WatchParameters struct {
// WatchCluster indicates to watch Cluster-related objects (Deployment, Pod, etc)
WatchCluster bool
// PromptMessage
PromptMessage string
}
// evaluateChangesFunc evaluates any file changes for the events by ignoring the files in fileIgnores slice and removes
@@ -444,14 +448,14 @@ func (o *WatchClient) processEvents(
}
} else {
fmt.Fprintf(out, "%s - %s\n\n", PushErrorString, err.Error())
PrintInfoMessage(out, path, parameters.StartOptions.WatchFiles, parameters.PromptMessage)
o.printInfoMessage(out, path, parameters.StartOptions.WatchFiles)
}
return nil
}
if oldStatus.GetState() != StateReady && componentStatus.GetState() == StateReady ||
!reflect.DeepEqual(oldStatus.EndpointsForwarded, componentStatus.EndpointsForwarded) {
PrintInfoMessage(out, path, parameters.StartOptions.WatchFiles, parameters.PromptMessage)
o.printInfoMessage(out, path, parameters.StartOptions.WatchFiles)
}
return nil
}
@@ -497,7 +501,7 @@ func removeDuplicates(input []string) []string {
return result
}
func PrintInfoMessage(out io.Writer, path string, watchFiles bool, promptMessage string) {
func (o *WatchClient) printInfoMessage(out io.Writer, path string, watchFiles bool) {
log.Sectionf("Dev mode")
if watchFiles {
fmt.Fprintf(
@@ -507,11 +511,9 @@ func PrintInfoMessage(out io.Writer, path string, watchFiles bool, promptMessage
path,
)
}
fmt.Fprintf(
fmt.Fprint(
out,
" %s%s",
log.Sbold("Keyboard Commands:"),
promptMessage,
o.informerClient.GetInfo(),
)
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/ActiveState/termtest/expect"
@@ -206,8 +205,7 @@ func StartDevMode(options DevSessionOpts) (devSession DevSession, err error) {
result.ErrOut = string(errContents)
result.Endpoints = getPorts(string(outContents), options.CustomAddress)
if options.StartAPIServer {
// errContents because the server message is still printed as a log/warning
result.APIServerEndpoint = getAPIServerPort(string(errContents))
result.APIServerEndpoint = getAPIServerPort(string(outContents))
}
return result, nil
@@ -395,10 +393,8 @@ func getPorts(s, address string) map[string]string {
}
// getAPIServerPort returns the address at which api server is running
//
// `I0617 11:40:44.124391 49578 starterserver.go:36] API Server started at localhost:20000/api/v1`
func getAPIServerPort(s string) string {
re := regexp.MustCompile(`(API Server started at localhost:[0-9]+\/api\/v1)`)
matches := re.FindString(s)
return strings.Split(matches, "at ")[1]
re := regexp.MustCompile(`API Server started at http://(localhost:[0-9]+\/api\/v1)`)
matches := re.FindStringSubmatch(s)
return matches[1]
}

View File

@@ -105,44 +105,6 @@ var _ = Describe("odo dev command with api server tests", func() {
Expect(resp.StatusCode).To(BeEquivalentTo(http.StatusOK))
})
It("should describe the API Server port in the experimental mode", func() {
args := []string{"describe", "component"}
if podman {
args = append(args, "--platform", "podman")
}
stdout := helper.Cmd("odo", args...).AddEnv("ODO_EXPERIMENTAL_MODE=true").ShouldPass().Out()
Expect(stdout).To(ContainSubstring("Dev Control Plane"))
Expect(stdout).To(ContainSubstring("API: http://%s", devSession.APIServerEndpoint))
if customPort {
Expect(stdout).To(ContainSubstring("Web UI: http://localhost:%d/", localPort))
} else {
Expect(stdout).To(MatchRegexp("Web UI: http:\\/\\/localhost:[0-9]+\\/"))
}
})
It("should describe the API Server port in the experimental mode (JSON)", func() {
args := []string{"describe", "component", "-o", "json"}
if podman {
args = append(args, "--platform", "podman")
}
stdout := helper.Cmd("odo", args...).AddEnv("ODO_EXPERIMENTAL_MODE=true").ShouldPass().Out()
helper.IsJSON(stdout)
helper.JsonPathExist(stdout, "devControlPlane")
plt := "cluster"
if podman {
plt = "podman"
}
helper.JsonPathContentHasLen(stdout, "devControlPlane", 1)
helper.JsonPathContentIs(stdout, "devControlPlane.0.platform", plt)
if customPort {
helper.JsonPathContentIs(stdout, "devControlPlane.0.localPort", strconv.Itoa(localPort))
} else {
helper.JsonPathContentIsValidUserPort(stdout, "devControlPlane.0.localPort")
}
helper.JsonPathContentIs(stdout, "devControlPlane.0.apiServerPath", "/api/v1/")
helper.JsonPathContentIs(stdout, "devControlPlane.0.webInterfacePath", "/")
})
It("should describe the API Server port", func() {
args := []string{"describe", "component"}
if podman {
@@ -151,7 +113,11 @@ var _ = Describe("odo dev command with api server tests", func() {
stdout := helper.Cmd("odo", args...).ShouldPass().Out()
Expect(stdout).To(ContainSubstring("Dev Control Plane"))
Expect(stdout).To(ContainSubstring("API: http://%s", devSession.APIServerEndpoint))
Expect(stdout).ToNot(ContainSubstring("Web UI: http://localhost:%d/", localPort))
if customPort {
Expect(stdout).To(ContainSubstring("Web UI: http://localhost:%d/", localPort))
} else {
Expect(stdout).To(MatchRegexp("Web UI: http:\\/\\/localhost:[0-9]+\\/"))
}
})
It("should describe the API Server port (JSON)", func() {
@@ -174,7 +140,7 @@ var _ = Describe("odo dev command with api server tests", func() {
helper.JsonPathContentIsValidUserPort(stdout, "devControlPlane.0.localPort")
}
helper.JsonPathContentIs(stdout, "devControlPlane.0.apiServerPath", "/api/v1/")
helper.JsonPathDoesNotExist(stdout, "devControlPlane.0.webInterfacePath")
helper.JsonPathContentIs(stdout, "devControlPlane.0.webInterfacePath", "/")
})
})
}

View File

@@ -33,3 +33,7 @@ div.error-message {
font-size: large;
margin: 16px;
}
.toplink {
color: white;
}

View File

@@ -1,8 +1,7 @@
<mat-toolbar color="primary">
<span>Devfile Builder</span>
<span class="spacer"></span>
<span class="topright">Work in progress</span>
<a mat-icon-button href="https://github.com/feloy/devfile-builder" target="_blank"><mat-icon svgIcon="github"></mat-icon></a>
<span class="topright"><a href="https://odo.dev/docs/user-guides/advanced/using-gui/" target="_blank" class="toplink">Work in progress</a></span>
<button style="top: -8px" data-cy="yaml-send" matTooltip="Save Devfile to disk" mat-flat-button color="warn" disabled="{{!(state.modified|async)}}" (click)="onSave(input.value)">Save</button>
</mat-toolbar>
<main>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 B

After

Width:  |  Height:  |  Size: 15 KiB