Skip to content

Commit 0bfefe7

Browse files
authored
feat: visibility flag for browse cmd (#323)
Add support for filtering executables by visibility. This will allow the inclusion of internal and hidden executables in the output when the flag is set to those values. The returned executables use a hierarchical match instead of exact (i.e. internal will also return private and public)
1 parent c924b26 commit 0bfefe7

File tree

8 files changed

+153
-30
lines changed

8 files changed

+153
-30
lines changed

cmd/internal/browse.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
execIO "github.com/flowexec/flow/internal/io/executable"
1414
"github.com/flowexec/flow/internal/io/library"
1515
"github.com/flowexec/flow/internal/logger"
16+
"github.com/flowexec/flow/types/common"
1617
"github.com/flowexec/flow/types/executable"
1718
)
1819

@@ -59,6 +60,7 @@ func RegisterBrowseCmd(ctx *context.Context, rootCmd *cobra.Command) {
5960
RegisterFlag(ctx, browseCmd, *flags.FilterTagFlag)
6061
RegisterFlag(ctx, browseCmd, *flags.FilterExecSubstringFlag)
6162
RegisterFlag(ctx, browseCmd, *flags.AllNamespacesFlag)
63+
RegisterFlag(ctx, browseCmd, *flags.VisibilityFlag)
6264
rootCmd.AddCommand(browseCmd)
6365
}
6466

@@ -106,6 +108,15 @@ func executableLibrary(ctx *context.Context, cmd *cobra.Command, _ []string) {
106108
tagsFilter := flags.ValueFor[[]string](cmd, *flags.FilterTagFlag, false)
107109
subStr := flags.ValueFor[string](cmd, *flags.FilterExecSubstringFlag, false)
108110

111+
visStr := flags.ValueFor[string](cmd, *flags.VisibilityFlag, false)
112+
visibilityFilter := common.VisibilityPrivate
113+
if visStr != "" {
114+
visibilityFilter = common.Visibility(visStr)
115+
if !isValidVisibility(visibilityFilter) {
116+
logger.Log().Fatalf("invalid visibility: %s (valid: public, private, internal, hidden)", visStr)
117+
}
118+
}
119+
109120
allExecs, err := ctx.ExecutableCache.GetExecutableList()
110121
if err != nil {
111122
logger.Log().FatalErr(err)
@@ -119,11 +130,12 @@ func executableLibrary(ctx *context.Context, cmd *cobra.Command, _ []string) {
119130
libraryModel := library.NewLibraryView(
120131
ctx, allWs, allExecs,
121132
library.Filter{
122-
Workspace: wsFilter,
123-
Namespace: nsFilter,
124-
Verb: executable.Verb(verbFilter),
125-
Tags: tagsFilter,
126-
Substring: subStr,
133+
Workspace: wsFilter,
134+
Namespace: nsFilter,
135+
Verb: executable.Verb(verbFilter),
136+
Tags: tagsFilter,
137+
Substring: subStr,
138+
Visibility: visibilityFilter,
127139
},
128140
io.Theme(ctx.Config.Theme.String()),
129141
runFunc,
@@ -154,13 +166,22 @@ func listExecutables(ctx *context.Context, cmd *cobra.Command, _ []string) {
154166
outputFormat := flags.ValueFor[string](cmd, *flags.OutputFormatFlag, false)
155167
substr := flags.ValueFor[string](cmd, *flags.FilterExecSubstringFlag, false)
156168

169+
visStr := flags.ValueFor[string](cmd, *flags.VisibilityFlag, false)
170+
visibilityFilter := common.VisibilityPrivate
171+
if visStr != "" {
172+
visibilityFilter = common.Visibility(visStr)
173+
if !isValidVisibility(visibilityFilter) {
174+
logger.Log().Fatalf("invalid visibility: %s (valid: public, private, internal, hidden)", visStr)
175+
}
176+
}
177+
157178
allExecs, err := ctx.ExecutableCache.GetExecutableList()
158179
if err != nil {
159180
logger.Log().FatalErr(err)
160181
}
161182
filteredExec := allExecs
162183
filteredExec = filteredExec.
163-
FilterByWorkspace(wsFilter).
184+
FilterByWorkspaceWithVisibility(wsFilter, visibilityFilter).
164185
FilterByNamespace(nsFilter).
165186
FilterByVerb(executable.Verb(verbFilter)).
166187
FilterByTags(tagsFilter).
@@ -219,3 +240,12 @@ func viewExecutable(ctx *context.Context, cmd *cobra.Command, args []string) {
219240
execIO.PrintExecutable(outputFormat, exec)
220241
}
221242
}
243+
244+
func isValidVisibility(v common.Visibility) bool {
245+
switch v {
246+
case common.VisibilityPublic, common.VisibilityPrivate, common.VisibilityInternal, common.VisibilityHidden:
247+
return true
248+
default:
249+
return false
250+
}
251+
}

cmd/internal/flags/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ var FilterVerbFlag = &Metadata{
7272
Required: false,
7373
}
7474

75+
var VisibilityFlag = &Metadata{
76+
Name: "visibility",
77+
Usage: "Filter by visibility level (hierarchical). Valid: public, private, internal, hidden. Default: private",
78+
Default: "",
79+
Required: false,
80+
}
81+
7582
var FilterTagFlag = &Metadata{
7683
Name: "tag",
7784
Shorthand: "t",

docs/cli/flow_browse.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ flow browse [EXECUTABLE-REFERENCE] [flags]
1919
### Options
2020

2121
```
22-
-a, --all List from all namespaces.
23-
-f, --filter string Filter executable by reference substring.
24-
-h, --help help for browse
25-
-l, --list Show a simple list view of executables instead of interactive discovery.
26-
-n, --namespace string Filter executables by namespace.
27-
-o, --output string Output format. One of: yaml, json, or tui. (default "tui")
28-
-t, --tag stringArray Filter by tags.
29-
-v, --verb string Filter executables by verb.
30-
-w, --workspace string Filter executables by workspace.
22+
-a, --all List from all namespaces.
23+
-f, --filter string Filter executable by reference substring.
24+
-h, --help help for browse
25+
-l, --list Show a simple list view of executables instead of interactive discovery.
26+
-n, --namespace string Filter executables by namespace.
27+
-o, --output string Output format. One of: yaml, json, or tui. (default "tui")
28+
-t, --tag stringArray Filter by tags.
29+
-v, --verb string Filter executables by verb.
30+
--visibility string Filter by visibility level (hierarchical). Valid: public, private, internal, hidden. Default: private
31+
-w, --workspace string Filter executables by workspace.
3132
```
3233

3334
### Options inherited from parent commands

internal/io/library/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (l *Library) setVisibleExecs() {
7575
filter := l.filter
7676
filteredExec := l.allExecutables
7777
filteredExec = filteredExec.
78-
FilterByWorkspace(curWs).
78+
FilterByWorkspaceWithVisibility(curWs, filter.Visibility).
7979
FilterByNamespace(curNs).
8080
FilterByVerb(filter.Verb).
8181
FilterByTags(filter.Tags).

internal/io/library/library.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type Filter struct {
4848
Verb executable.Verb
4949
Tags common.Tags
5050
Substring string
51+
Visibility common.Visibility
5152
}
5253

5354
func NewLibrary(

types/common/common.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,22 @@ func (v Visibility) IsInternal() bool {
7373
func (v Visibility) IsHidden() bool {
7474
return v == VisibilityHidden
7575
}
76+
77+
// Level returns the hierarchical level of the visibility for filtering comparisons.
78+
// Hierarchy (most to least visible):
79+
// 1: public, 2: private, 3: internal, 4: hidden
80+
// Defaults to private when unknown.
81+
func (v Visibility) Level() int {
82+
switch v {
83+
case VisibilityPublic:
84+
return 1
85+
case VisibilityPrivate:
86+
return 2
87+
case VisibilityInternal:
88+
return 3
89+
case VisibilityHidden:
90+
return 4
91+
default:
92+
return 2
93+
}
94+
}

types/executable/executable.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -288,19 +288,25 @@ func (e *Executable) MergeTags(tags common.Tags) {
288288
e.Tags = slices.Compact(append(e.Tags, tags...))
289289
}
290290

291-
// IsVisibleFromWorkspace returns true if the executable should be shown in terminal output for the given workspace.
292-
func (e *Executable) IsVisibleFromWorkspace(workspaceFilter string) bool {
293-
matchesWsFiler := e.Workspace() == workspaceFilter || workspaceFilter == "" || workspaceFilter == WildcardWorkspace
294-
if e.Visibility == nil {
295-
return matchesWsFiler
291+
// IsVisibleFromWorkspace returns true if the executable should be shown in the UI for the given workspace
292+
// and visibility filter.
293+
func (e *Executable) IsVisibleFromWorkspace(workspaceFilter string, visibilityFilter common.Visibility) bool {
294+
matchesWs := e.Workspace() == workspaceFilter || workspaceFilter == "" || workspaceFilter == WildcardWorkspace
295+
296+
execVis := common.VisibilityPrivate
297+
if e.Visibility != nil {
298+
execVis = common.Visibility(*e.Visibility)
296299
}
297-
switch common.Visibility(*e.Visibility) {
298-
case common.VisibilityPrivate:
299-
return matchesWsFiler
300+
301+
switch execVis {
300302
case common.VisibilityPublic:
301-
return true
302-
case common.VisibilityInternal, common.VisibilityHidden:
303-
return false
303+
return visibilityFilter.Level() >= common.VisibilityPublic.Level()
304+
case common.VisibilityPrivate:
305+
return matchesWs && visibilityFilter.Level() >= common.VisibilityPrivate.Level()
306+
case common.VisibilityInternal:
307+
return matchesWs && visibilityFilter.Level() >= common.VisibilityInternal.Level()
308+
case common.VisibilityHidden:
309+
return matchesWs && visibilityFilter.Level() >= common.VisibilityHidden.Level()
304310
default:
305311
return false
306312
}
@@ -444,9 +450,13 @@ func (l ExecutableList) FilterBySubstring(str string) ExecutableList {
444450
}
445451

446452
func (l ExecutableList) FilterByWorkspace(ws string) ExecutableList {
453+
return l.FilterByWorkspaceWithVisibility(ws, common.VisibilityPrivate)
454+
}
455+
456+
func (l ExecutableList) FilterByWorkspaceWithVisibility(ws string, vis common.Visibility) ExecutableList {
447457
executables := make(ExecutableList, 0)
448458
for _, exec := range l {
449-
if exec.IsVisibleFromWorkspace(ws) {
459+
if exec.IsVisibleFromWorkspace(ws, vis) {
450460
executables = append(executables, exec)
451461
}
452462
}

types/executable/executables_test.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,9 @@ var _ = Describe("Executable", func() {
122122
v := executable.ExecutableVisibility(*visibility)
123123
exec.Visibility = &v
124124
if wsMatch {
125-
Expect(exec.IsVisibleFromWorkspace(testWsName)).To(Equal(expected))
125+
Expect(exec.IsVisibleFromWorkspace(testWsName, common.VisibilityPrivate)).To(Equal(expected))
126126
} else {
127-
Expect(exec.IsVisibleFromWorkspace("another-ws")).To(Equal(expected))
127+
Expect(exec.IsVisibleFromWorkspace("another-ws", common.VisibilityPrivate)).To(Equal(expected))
128128
}
129129
},
130130
Entry("public from ws", common.VisibilityPublic.NewPointer(), true, true),
@@ -330,3 +330,58 @@ var _ = DescribeTable("HasFlowFileTemplateExt", func(file string, expected bool)
330330
Entry("ends with .flow.tmpl + something else", "development.flow.tmpl.txt", false),
331331
Entry("ends with something else", "development.flow.txt", false),
332332
)
333+
334+
var _ = Describe("Executable Visibility", func() {
335+
It("should show public executables with any filter", func() {
336+
ws := "ws1"
337+
vis := common.VisibilityPublic
338+
ex := &executable.Executable{Visibility: (*executable.ExecutableVisibility)(&vis)}
339+
ex.SetContext(ws, "path", "", "file.flow")
340+
Expect(ex.IsVisibleFromWorkspace("ws2", common.VisibilityPublic)).To(BeTrue())
341+
Expect(ex.IsVisibleFromWorkspace("ws2", common.VisibilityPrivate)).To(BeTrue())
342+
Expect(ex.IsVisibleFromWorkspace("ws2", common.VisibilityInternal)).To(BeTrue())
343+
})
344+
345+
It("should respect workspace scoping for private", func() {
346+
ws := "ws1"
347+
vis := common.VisibilityPrivate
348+
ex := &executable.Executable{Visibility: (*executable.ExecutableVisibility)(&vis)}
349+
ex.SetContext(ws, "path", "", "file.flow")
350+
Expect(ex.IsVisibleFromWorkspace(ws, common.VisibilityPrivate)).To(BeTrue())
351+
Expect(ex.IsVisibleFromWorkspace("ws2", common.VisibilityPrivate)).To(BeFalse())
352+
})
353+
354+
It("should hide private with public filter", func() {
355+
ws := "ws1"
356+
vis := common.VisibilityPrivate
357+
ex := &executable.Executable{Visibility: (*executable.ExecutableVisibility)(&vis)}
358+
ex.SetContext(ws, "path", "", "file.flow")
359+
Expect(ex.IsVisibleFromWorkspace(ws, common.VisibilityPublic)).To(BeFalse())
360+
})
361+
362+
It("should hide internal without internal+ filter", func() {
363+
ws := "ws1"
364+
vis := common.VisibilityInternal
365+
ex := &executable.Executable{Visibility: (*executable.ExecutableVisibility)(&vis)}
366+
ex.SetContext(ws, "path", "", "file.flow")
367+
Expect(ex.IsVisibleFromWorkspace(ws, common.VisibilityPrivate)).To(BeFalse())
368+
Expect(ex.IsVisibleFromWorkspace(ws, common.VisibilityInternal)).To(BeTrue())
369+
})
370+
371+
It("should respect workspace scoping for internal", func() {
372+
ws := "ws1"
373+
vis := common.VisibilityInternal
374+
ex := &executable.Executable{Visibility: (*executable.ExecutableVisibility)(&vis)}
375+
ex.SetContext(ws, "path", "", "file.flow")
376+
Expect(ex.IsVisibleFromWorkspace("ws2", common.VisibilityInternal)).To(BeFalse())
377+
})
378+
379+
It("should only show hidden with hidden filter", func() {
380+
ws := "ws1"
381+
vis := common.VisibilityHidden
382+
ex := &executable.Executable{Visibility: (*executable.ExecutableVisibility)(&vis)}
383+
ex.SetContext(ws, "path", "", "file.flow")
384+
Expect(ex.IsVisibleFromWorkspace(ws, common.VisibilityInternal)).To(BeFalse())
385+
Expect(ex.IsVisibleFromWorkspace(ws, common.VisibilityHidden)).To(BeTrue())
386+
})
387+
})

0 commit comments

Comments
 (0)