Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add navigation menu with in compose up (attached) #11605

Merged
merged 1 commit into from
Mar 22, 2024

Conversation

jhrotko
Copy link
Contributor

@jhrotko jhrotko commented Mar 12, 2024

What I did
We want to open Docker Desktop through a shortcut in docker compose up.

To use this feature run

docker compose up

... yes it comes as default for attached mode :). it will render the navigation menu at the bottom.
You can disable this feature while using up run:

docker compose up --menu=[false|true]

OR set environment var COMPOSE_MENU=[false|true]
OR use Docker Desktop experimental features and enable/disable - Compose: TUI nav bar

Screen.Recording.2024-03-16.at.17.01.26.mov

When an error occurs, it will be displayed at the top of the navigation menu for 10s.
We are also sending telemetry to understand if users' usage of this new feature. PR merged in pinata: https://github.com/docker/pinata/pull/27207

This PR introduces a keyboard listener that is currently listening to the following events:

  • CTRL+C
  • ENTER
  • w -> Start watch mode (alternatively you can also start watch mode with docker compose up --watch)
  • v -> Open compose project in Docker Desktop through a deeplink docker-desktop://dashboard/apps/NAME
Screenshot 2024-03-12 at 13 04 57

Related issue

https://docker.atlassian.net/jira/software/c/projects/COMP/boards/576?selectedIssue=COMP-417

(not mandatory) A picture of a cute animal, if possible in relation to what you did

@jhrotko jhrotko changed the title Open dd compose project Add nav bar with info and error message in compose up (attached) Mar 12, 2024
@jhrotko jhrotko self-assigned this Mar 12, 2024
case keyboard.KeyCtrlC:
keyboard.Close()
formatter.KeyboardInfo.ClearInfo()
gracefulTeardown()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about the impact doing so as context is not cancelled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can I test this scenario? What is expected if the context is not cancelled?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. We rely on <-ctx.Done() in a few place to stop goroutines after Ctrl+C, typically to stop collecting logs, watch container events, etc. I assume after the application stopped those will be released anyway, but this might bring some new race conditions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be okay here (although it makes me think the graceful termination code/doneCh/ctx.Done() handling could use a refactor).

I wonder if cancelling the context here would be better.

Also I wonder, since we're already capturing SIGINT above (when we do signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM), if the new keyboard stuff is going to interfere with that.

Copy link
Contributor Author

@jhrotko jhrotko Mar 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -81,6 +83,7 @@ require (
github.com/docker/docker-credential-helpers v0.8.0 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably does what we expect, by I wonder we want to rely on a dependency which hasn't been updated for 2 years

Comment on lines 39 to 228
func (lk *LogKeyboard) createBuffer() {
fmt.Print("\012") // new line
fmt.Print("\012")
fmt.Print("\033[2A") // go back 3 lines
}

func (lk *LogKeyboard) printInfo() {
height := goterm.Height()
fmt.Print("\0337") // save cursor position
if lk.err != nil {
fmt.Printf("\033[%d;0H", height-1) // Move to before last line
fmt.Printf("\033[K" + errorColor + "[Error] " + lk.err.Error())
}
fmt.Printf("\033[%d;0H", height) // Move to last line
// clear line
fmt.Print("\033[K" + navColor(" >> [CTRL+G] open project in Docker Desktop [$] get more features"))
fmt.Print("\0338") // restore cursor position
}

func (lk *LogKeyboard) ClearInfo() {
height := goterm.Height()
fmt.Print("\0337") // save cursor position
if lk.err != nil {
fmt.Printf("\033[%d;0H", height-1)
fmt.Print("\033[2K") // clear line
}
fmt.Printf("\033[%d;0H", height) // Move to last line
fmt.Print("\033[2K") // clear line
fmt.Print("\0338") // restore cursor position
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking/not a blocker, but I'm starting to wonder if we should think about migrating over to an actual TUI lib as opposed to rolling our own. bubbletea is pretty nice for this kind of stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, we should consider this lib for a further interaction if this feature gets traction

fmt.Print("\033[2K") // clear line
}
fmt.Printf("\033[%d;0H", height) // Move to last line
fmt.Print("\033[2K") // clear line
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be more readable to put all these hieroglyphs in consts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did huge refactor on this file. Was still work in progress at the time. It should be more readable now :)

@laurazard
Copy link
Member

Looks really good overall! Happy with the "TUI" improvements 🥳

I think that's the plan from what @jhrotko has mentioned, but I wanted to confirm that the idea is to use #11593 and only enable this when running this on a Docker Desktop environment (cc @milas), so that people running this headless/on bare Linux w/o DD don't get confused.

@jhrotko jhrotko force-pushed the open-dd-compose-project branch 2 times, most recently from 3bafd93 to 3a009bc Compare March 13, 2024 11:45
@@ -127,6 +128,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy")
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
flags.BoolVar(&up.navigationBar, "navigation-menu", true, "While running in attach mode, enable shortcuts and shortcuts info bar.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"shortcuts and shortcuts" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a fix. Let me know what do you think

@jhrotko jhrotko changed the title Add nav bar with info and error message in compose up (attached) Add navigation menu with in compose up (attached) Mar 16, 2024
@jhrotko jhrotko marked this pull request as ready for review March 16, 2024 20:18
@jhrotko jhrotko force-pushed the open-dd-compose-project branch 4 times, most recently from b0d1d2d to 11189b2 Compare March 18, 2024 10:39
Copy link

codecov bot commented Mar 18, 2024

Codecov Report

Attention: Patch coverage is 19.69231% with 261 lines in your changes are missing coverage. Please review.

Project coverage is 54.75%. Comparing base (3950460) to head (831a5af).

Files Patch % Lines
cmd/formatter/shortcut.go 0.00% 192 Missing ⚠️
cmd/formatter/ansi.go 16.12% 26 Missing ⚠️
pkg/compose/up.go 33.33% 10 Missing and 2 partials ⚠️
cmd/formatter/colors.go 0.00% 8 Missing ⚠️
cmd/formatter/logs.go 60.00% 2 Missing and 4 partials ⚠️
internal/tracing/keyboard_metrics.go 45.45% 4 Missing and 2 partials ⚠️
pkg/compose/watch.go 73.33% 3 Missing and 1 partial ⚠️
cmd/compose/compose.go 70.00% 1 Missing and 2 partials ⚠️
internal/tracing/wrap.go 75.00% 1 Missing ⚠️
pkg/compose/compose.go 66.66% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11605      +/-   ##
==========================================
- Coverage   55.62%   54.75%   -0.87%     
==========================================
  Files         142      145       +3     
  Lines       12258    12553     +295     
==========================================
+ Hits         6818     6874      +56     
- Misses       4752     4984     +232     
- Partials      688      695       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -101,6 +101,7 @@ RUN --mount=type=bind,target=. \
FROM build-base AS test
ARG CGO_ENABLED=0
ARG BUILD_TAGS
ENV COMPOSE_MENU=FALSE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary for building?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, because by default it should be true, in order not to break tests this is easier to set the COMPOSE_MENU false in all containers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as a follow-up) We can have the e2e tests set this when they exec Compose to avoid needing it globally like this

Comment on lines 76 to 98
return fmt.Sprintf("%s%s%s", ansiColorCode(code), s, ansiColorCode("0"))
}

func ansi(code string) string {
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
func ansiColorCode(code string) string {
return fmt.Sprintf("\033[%sm", code)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't realize we had this – isn't the stuff in aec enough? I think we use it sometimes

DoneColor colorFunc = aec.BlueF.Apply
TimerColor colorFunc = aec.BlueF.Apply
CountColor colorFunc = aec.YellowF.Apply
WarningColor colorFunc = aec.YellowF.With(aec.Bold).Apply
SuccessColor colorFunc = aec.GreenF.Apply
ErrorColor colorFunc = aec.RedF.With(aec.Bold).Apply
PrefixColor colorFunc = aec.CyanF.Apply

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then it's used like

SuccessColor(strings.Join(completion, "")),

Copy link
Contributor Author

@jhrotko jhrotko Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to add the bold to the prefix color :( \033[36m -> cyan vs \033[1;36m -> bold + cyan. The prefix color is not abstract enough for me to add the bold format

@jhrotko jhrotko force-pushed the open-dd-compose-project branch 2 times, most recently from 43a5fb5 to aec5805 Compare March 19, 2024 17:12
@@ -181,6 +186,9 @@ func runUp(
if err != nil {
return err
}
if !upOptions.navigationMenuChanged {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems bit complicated to manage value set by env var, is there any drawback doing the same as https://github.com/docker/compose/blob/main/cmd/compose/up.go#L109-L110 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this comment is mine, I was connected with by @docker.com email while commenting :P)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also want to have into consideration the value of the Docker Desktop experimental feature value
Screenshot 2024-03-21 at 16 05 49

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh indeed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not for this PR)

We definitely need to figure out a re-usable pattern for these types of use cases to simplify driving experimental settings via Docker Desktop and combining with flags/options in Compose

Our init sequence has gotten pretty complex between Docker CLI plugin framework + Cobra + Compose itself 😓

@jhrotko jhrotko force-pushed the open-dd-compose-project branch 6 times, most recently from c7f443b to 33b4f1d Compare March 21, 2024 18:01
Copy link
Contributor

@milas milas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - few random comments, biggest question is over that one conditional but otherwise no major concerns from me

@@ -101,6 +101,7 @@ RUN --mount=type=bind,target=. \
FROM build-base AS test
ARG CGO_ENABLED=0
ARG BUILD_TAGS
ENV COMPOSE_MENU=FALSE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as a follow-up) We can have the e2e tests set this when they exec Compose to avoid needing it globally like this

@@ -181,6 +186,9 @@ func runUp(
if err != nil {
return err
}
if !upOptions.navigationMenuChanged {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not for this PR)

We definitely need to figure out a re-usable pattern for these types of use cases to simplify driving experimental settings via Docker Desktop and combining with flags/options in Compose

Our init sequence has gotten pretty complex between Docker CLI plugin framework + Cobra + Compose itself 😓

timeStart time.Time
}

func (ke *KeyboardError) shoudlDisplay() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/shoudlDisplay/shouldDisplay


MoveCursor(height-linesOffset(menu), 0)
ClearLine()
fmt.Print(menu)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(for follow-up)

You can make it easier to test this by storing stdout/io.Writer on the type and writing to that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean, instead of using fmt.Print first pipe all the characters to stdout and then print?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add an io.Writer field in KeyboardError type, and use fmt.Fprint. This will allow unit tests to pass a bytes.Buffer as writer and check output
For actual use, should be initialized to dockercli.Out() to respect docker CLI plugin architecture

}
}

func (lk *LogKeyboard) navigationMenu() string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi strings.Builder can sometimes be handy for these types of finicky formatting formatting type functions

(this is readable/fine as-is! no need to change, just sharing)

https://pkg.go.dev/strings#Builder

Comment on lines 257 to 259
lk.Watch.switchWatching()
if !lk.Watch.isWatching() && lk.Watch.Cancel != nil {
lk.Watch.Cancel()
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be

	lk.Watch.switchWatching()
	if !lk.Watch.isWatching() {
		if lk.Watch.Cancel != nil {
			lk.Watch.Cancel()
		}
	} else {

(that is, even if the cancel function is nil, we only want to start watch if isWatching == true, right?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think this was an old bug that was fixed, I think it was because the configuration validation was being done after and not before at some point. Both expressions are equivalent

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

@@ -320,3 +320,7 @@ func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
return runtimeVersion.val, runtimeVersion.err

}

func (s *composeService) isDesktopIntegrationActive() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored other places in the code that also did this validation

@@ -124,7 +146,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err
})

if options.Start.Watch {
if options.Start.Watch && !options.Start.NavigationMenu {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC --watch would then not start watch mode until menu is disabled. Why ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this is being managed inside Keyboard Manager, I need the context to start/stop watch mode afterwards

Copy link
Contributor Author

@jhrotko jhrotko Mar 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right. yet more spaghetti code to our already confusing "architecture" 😅

if !watching {
return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section")
}
options.LogTo.Log(api.WatchLogger, "watch enabled")
options.LogTo.Log(api.WatchLogger, "Watch started")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally preferred "enabled", but that's just me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, even the navigation menu message switches between watch enabled/disabled

@jhrotko jhrotko force-pushed the open-dd-compose-project branch 5 times, most recently from 4ed8a30 to fc8b195 Compare March 22, 2024 16:26
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
@ndeloof ndeloof enabled auto-merge (rebase) March 22, 2024 16:31
@ndeloof ndeloof merged commit e9dc820 into docker:main Mar 22, 2024
32 checks passed
matt9ucci added a commit to matt9ucci/DockerCompletion that referenced this pull request Apr 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants