Skip to content

Commit b7d689b

Browse files
authored
feat: Enhance scan with options UX/UI (#86)
* init scan with options enhancements * feat: enhance scan with options dialog * feat: unify config module getters and setters, create .scanoss folder if does not exist, enhance ux/ui for scanning * fix: config initialization * feat: ES-140 add config change listeners * feat: ES-140 fix small bug when updating settings file in-memory struct * feat: ES-140 Add advanced options input * feat: ES-140 modify placeholder * feat: ES-140 Update changelog * feat: ES-140 Check errors, improve type assertions
1 parent 30dcdf4 commit b7d689b

32 files changed

+801
-313
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ node_modules
55
frontend/dist
66
.idea
77
.vscode
8+
.cursor
89

910
.scanoss
1011
scanoss.json

CHANGELOG.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Upcoming changes...
1212

13-
## [0.5.0]
13+
## [0.6.0] 2024-02-03
14+
15+
### Added
16+
- Add sort options for results:
17+
- By path
18+
- By match percentage
19+
- Add empty state for results
20+
- Add scan root selector and info in top bar
21+
22+
23+
### Fixed
24+
- Improved diff viewer scrolling performance
25+
- Show help command without running the process
26+
- Check for several python default install locations when running scan command
27+
- Set proper scan root when executing the app from symlink
28+
29+
### Changed
30+
- Use same color for left and right code viewers
31+
- "Scan With Options" improvements
32+
- Add advanced options input
33+
- Do not allow to manually select output path
34+
- Show boolean options as checkboxes
35+
- Hide console output when no lines are available
36+
- Create .scanoss directory if it does not exist
37+
38+
### Removed
39+
- Remove "Scan Current Directory" menu option
40+
41+
42+
43+
## [0.5.0] - 2024-01-24
1444
### Added
1545
- Initial open source release
1646
- React-based frontend for diff viewer
@@ -21,3 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2151
- Keyboard shortcuts support
2252
- Terminal output viewer
2353
- Configuration management through CLI
54+
55+
56+
[0.5.0]: https://github.com/scanoss/scanoss.cc/compare/v0.4.0...v0.5.0
57+
[0.6.0]: https://github.com/scanoss/scanoss.cc/compare/v0.5.0...v0.6.0

app.go

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ package main
2626
import (
2727
"context"
2828
"fmt"
29-
"os"
3029
"slices"
3130

3231
"github.com/rs/zerolog/log"
@@ -43,6 +42,7 @@ type App struct {
4342
ctx context.Context
4443
scanossSettingsService service.ScanossSettingsService
4544
keyboardService service.KeyboardService
45+
cfg *config.Config
4646
}
4747

4848
func NewApp() *App {
@@ -53,15 +53,16 @@ func (a *App) Init(ctx context.Context, scanossSettingsService service.ScanossSe
5353
a.ctx = ctx
5454
a.scanossSettingsService = scanossSettingsService
5555
a.keyboardService = keyboardService
56+
a.cfg = config.GetInstance()
5657
a.startup()
5758
}
5859

5960
func (a *App) startup() {
6061
a.maybeSetWindowTitle()
6162
a.initializeMenu()
62-
log.Debug().Msgf("Scan Settings file path: %s", config.GetInstance().ScanSettingsFilePath)
63-
log.Debug().Msgf("Results file path: %s", config.GetInstance().ResultFilePath)
64-
log.Debug().Msgf("Scan Root file path: %s", config.GetInstance().ScanRoot)
63+
log.Debug().Msgf("Scan Settings file path: %s", a.cfg.GetScanSettingsFilePath())
64+
log.Debug().Msgf("Results file path: %s", a.cfg.GetResultFilePath())
65+
log.Debug().Msgf("Scan Root file path: %s", a.cfg.GetScanRoot())
6566
log.Info().Msgf("App Version: %s", entities.AppVersion)
6667
}
6768

@@ -186,31 +187,26 @@ func (a *App) SelectFile(defaultDir string) (string, error) {
186187
return filePath, nil
187188
}
188189

189-
func (a *App) GetWorkingDir() string {
190-
workingDir, _ := os.Getwd()
191-
return workingDir
192-
}
193-
194190
func (a *App) GetScanRoot() (string, error) {
195-
return config.GetInstance().ScanRoot, nil
191+
return a.cfg.GetScanRoot(), nil
196192
}
197193

198194
func (a *App) GetResultFilePath() (string, error) {
199-
return config.GetInstance().ResultFilePath, nil
195+
return a.cfg.GetResultFilePath(), nil
200196
}
201197

202198
func (a *App) GetScanSettingsFilePath() (string, error) {
203-
return config.GetInstance().ScanSettingsFilePath, nil
199+
return a.cfg.GetScanSettingsFilePath(), nil
204200
}
205201

206202
func (a *App) SetScanRoot(path string) {
207-
config.GetInstance().SetScanRoot(path)
203+
a.cfg.SetScanRoot(path)
208204
}
209205

210206
func (a *App) SetResultFilePath(path string) {
211-
config.GetInstance().SetResultFilePath(path)
207+
a.cfg.SetResultFilePath(path)
212208
}
213209

214210
func (a *App) SetScanSettingsFilePath(path string) {
215-
config.GetInstance().SetScanSettingsFilePath(path)
211+
a.cfg.SetScanSettingsFilePath(path)
216212
}

backend/entities/scan.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,44 @@ type ScanResponse struct {
2828
ErrOutput string `json:"error_output,omitempty"`
2929
Error error `json:"error,omitempty"`
3030
}
31+
32+
type ScanArgDef struct {
33+
Name string
34+
Shorthand string
35+
Default interface{}
36+
Usage string
37+
Type string
38+
IsCore bool
39+
IsFileSelector bool
40+
}
41+
42+
var (
43+
ScanArguments = []ScanArgDef{
44+
{"wfp", "w", "", "Scan a WFP File instead of a folder (optional)", "string", false, true},
45+
{"dep", "p", "", "Use a dependency file instead of a folder (optional)", "string", false, true},
46+
{"stdin", "s", "", "Scan the file contents supplied via STDIN (optional)", "string", false, false},
47+
{"files", "e", []string{}, "List of files to scan.", "stringSlice", false, false},
48+
{"identify", "i", "", "Scan and identify components in SBOM file", "string", false, true},
49+
{"ignore", "n", "", "Ignore components specified in the SBOM file", "string", false, true},
50+
{"output", "o", "", "Output result file name (optional - default stdout).", "string", true, true},
51+
{"format", "f", "plain", "Result output format (optional - default: plain)", "string", false, false},
52+
{"threads", "T", 5, "Number of threads to use while scanning (optional - default 5)", "int", false, false},
53+
{"flags", "F", 0, "Scanning engine flags", "int", false, false},
54+
{"post-size", "P", 32, "Number of kilobytes to limit the post to while scanning (optional - default 32)", "int", false, false},
55+
{"timeout", "M", 180, "Timeout (in seconds) for API communication (optional - default 180)", "int", false, false},
56+
{"retry", "R", 5, "Retry limit for API communication (optional - default 5)", "int", false, false},
57+
{"no-wfp-output", "", false, "Skip WFP file generation", "bool", false, false},
58+
{"dependencies", "D", false, "Add Dependency scanning", "bool", false, false},
59+
{"dependencies-only", "", false, "Run Dependency scanning only", "bool", false, false},
60+
{"sc-command", "", "", "Scancode command and path if required (optional - default scancode).", "string", false, false},
61+
{"sc-timeout", "", 600, "Timeout (in seconds) for scancode to complete (optional - default 600)", "int", false, false},
62+
{"dep-scope", "", "", "Filter dependencies by scope - default all (options: dev/prod)", "string", false, false},
63+
{"dep-scope-inc", "", "", "Include dependencies with declared scopes", "string", false, false},
64+
{"dep-scope-exc", "", "", "Exclude dependencies with declared scopes", "string", false, false},
65+
{"settings", "", "", "Settings file to use for scanning (optional - default scanoss.json)", "string", true, true},
66+
{"skip-settings-file", "", false, "Skip default settings file (scanoss.json) if it exists", "bool", false, false},
67+
{"debug", "d", false, "Enable debug messages", "bool", true, false},
68+
{"trace", "t", false, "Enable trace messages, including API posts", "bool", true, false},
69+
{"quiet", "q", true, "Enable quiet mode", "bool", true, false},
70+
}
71+
)

backend/repository/component_repository_json_impl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func NewJSONComponentRepository(fr utils.FileReader) *JSONComponentRepository {
5757
}
5858

5959
func (r *JSONComponentRepository) FindByFilePath(path string) (entities.Component, error) {
60-
resultFilePath := config.GetInstance().ResultFilePath
60+
resultFilePath := config.GetInstance().GetResultFilePath()
6161

6262
resultFileBytes, err := r.fr.ReadFile(resultFilePath)
6363
if err != nil {

backend/repository/file_repository_impl.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func NewFileRepositoryImpl() FileRepository {
4141
}
4242

4343
func (r *FileRepositoryImpl) ReadLocalFile(path string) (entities.File, error) {
44-
scanRootPath := config.GetInstance().ScanRoot
44+
scanRootPath := config.GetInstance().GetScanRoot()
4545

4646
absolutePath := filepath.Join(scanRootPath, path)
4747

@@ -54,8 +54,10 @@ func (r *FileRepositoryImpl) ReadLocalFile(path string) (entities.File, error) {
5454
}
5555

5656
func (r *FileRepositoryImpl) ReadRemoteFileByMD5(path string, md5 string) (entities.File, error) {
57-
baseURL := config.GetInstance().ApiUrl
58-
token := config.GetInstance().ApiToken
57+
cfg := config.GetInstance()
58+
59+
baseURL := cfg.GetApiUrl()
60+
token := cfg.GetApiToken()
5961

6062
url := fmt.Sprintf("%s/file_contents/%s", baseURL, md5)
6163

backend/repository/file_repository_impl_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestFileRepositoryImpl(t *testing.T) {
4949
err := os.WriteFile(absolutePath, testContent, 0644)
5050
assert.NoError(t, err)
5151

52-
config.GetInstance().ScanRoot = currentPath
52+
config.GetInstance().SetScanRoot(currentPath)
5353

5454
repo := NewFileRepositoryImpl()
5555

backend/repository/result_repository_json_impl.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ package repository
2525

2626
import (
2727
"encoding/json"
28+
"os"
2829

2930
"github.com/rs/zerolog/log"
3031
"github.com/scanoss/scanoss.cc/backend/entities"
@@ -43,10 +44,12 @@ func NewResultRepositoryJsonImpl(fr utils.FileReader) *ResultRepositoryJsonImpl
4344
}
4445

4546
func (r *ResultRepositoryJsonImpl) GetResults(filter entities.ResultFilter) ([]entities.Result, error) {
46-
// Path to your JSON file
47-
resultFilePath := config.GetInstance().ResultFilePath
47+
resultFilePath := config.GetInstance().GetResultFilePath()
4848
resultByte, err := r.fr.ReadFile(resultFilePath)
4949
if err != nil {
50+
if os.IsNotExist(err) {
51+
return []entities.Result{}, nil
52+
}
5053
log.Error().Err(err).Msg("Error reading result file")
5154
return []entities.Result{}, entities.ErrReadingResultFile
5255
}

backend/repository/scanoss_settings_repository_json_impl.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,63 @@ func NewScanossSettingsJsonRepository(fr utils.FileReader) ScanossSettingsReposi
4747
}
4848

4949
func (r *ScanossSettingsJsonRepository) Init() error {
50-
r.mutex.Lock()
51-
defer r.mutex.Unlock()
50+
cfg := config.GetInstance()
51+
52+
if cfg == nil {
53+
err := fmt.Errorf("config is not initialized")
54+
log.Error().Err(err).Msg("Error initializing ScanossSettingsJsonRepository")
55+
return err
56+
}
57+
58+
r.setSettingsFile(cfg.GetScanSettingsFilePath())
59+
60+
cfg.RegisterListener(r.onConfigChange)
61+
62+
return nil
63+
}
5264

65+
// Triggered when the config is changed (e.g. scan root, scan settings file path, etc.)
66+
func (r *ScanossSettingsJsonRepository) onConfigChange(newCfg *config.Config) {
67+
r.setSettingsFile(newCfg.GetScanSettingsFilePath())
68+
}
69+
70+
func (r *ScanossSettingsJsonRepository) setSettingsFile(path string) {
5371
sf, err := r.Read()
72+
5473
if err != nil {
55-
log.Fatal().Err(err).Msgf("Error reading settings file: %v", config.GetInstance().ScanSettingsFilePath)
56-
return err
74+
log.Error().Err(err).Msgf("Error reading settings file: %v", path)
75+
return
5776
}
5877

59-
entities.ScanossSettingsJson = &entities.ScanossSettings{
60-
SettingsFile: &sf,
78+
if entities.ScanossSettingsJson == nil {
79+
entities.ScanossSettingsJson = &entities.ScanossSettings{
80+
SettingsFile: &sf,
81+
}
6182
}
6283

63-
return nil
84+
entities.ScanossSettingsJson.SettingsFile = &sf
6485
}
6586

6687
func (r *ScanossSettingsJsonRepository) Save() error {
88+
r.mutex.Lock()
89+
defer r.mutex.Unlock()
90+
6791
sf := r.GetSettings()
68-
if err := utils.WriteJsonFile(config.GetInstance().ScanSettingsFilePath, sf); err != nil {
92+
if err := utils.WriteJsonFile(config.GetInstance().GetScanSettingsFilePath(), sf); err != nil {
6993
return err
7094
}
7195
return nil
7296
}
7397

7498
func (r *ScanossSettingsJsonRepository) Read() (entities.SettingsFile, error) {
75-
if config.GetInstance() == nil {
99+
r.mutex.RLock()
100+
defer r.mutex.RUnlock()
101+
102+
cfg := config.GetInstance()
103+
if cfg == nil {
76104
return entities.SettingsFile{}, fmt.Errorf("config is not initialized")
77105
}
78-
scanSettingsFileBytes, err := r.fr.ReadFile(config.GetInstance().ScanSettingsFilePath)
106+
scanSettingsFileBytes, err := r.fr.ReadFile(cfg.GetScanSettingsFilePath())
79107
if err != nil {
80108
if errors.Is(err, os.ErrNotExist) {
81109
return entities.SettingsFile{}, nil

backend/service/scan_service_python_cli_impl.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import (
3030
"io"
3131
"os"
3232
"os/exec"
33+
"path/filepath"
3334

35+
"github.com/rs/zerolog/log"
36+
"github.com/scanoss/scanoss.cc/backend/entities"
3437
"github.com/scanoss/scanoss.cc/internal/config"
3538
"github.com/wailsapp/wails/v2/pkg/runtime"
3639
)
@@ -112,6 +115,9 @@ func (s *ScanServicePythonImpl) executeScanWithPipes(args []string) (*exec.Cmd,
112115

113116
cmdArgs = append(cmdArgs, sensitiveArgs...)
114117

118+
// If the output folder does not exist, create it. This should be handled by the python cli
119+
s.maybeCreateOutputFolder(args)
120+
115121
cmd := exec.Command(s.cmd, cmdArgs...)
116122

117123
stdout, err := cmd.StdoutPipe()
@@ -182,12 +188,12 @@ func (s *ScanServicePythonImpl) GetDefaultScanArgs() []string {
182188
args := []string{}
183189
cfg := config.GetInstance()
184190

185-
if cfg.ResultFilePath != "" {
186-
args = append(args, "--output", cfg.ResultFilePath)
191+
if cfg.GetResultFilePath() != "" {
192+
args = append(args, "--output", cfg.GetResultFilePath())
187193
}
188194

189-
if cfg.ScanSettingsFilePath != "" {
190-
args = append(args, "--settings", cfg.ScanSettingsFilePath)
195+
if cfg.GetScanSettingsFilePath() != "" {
196+
args = append(args, "--settings", cfg.GetScanSettingsFilePath())
191197
}
192198

193199
return args
@@ -197,12 +203,12 @@ func (s *ScanServicePythonImpl) GetSensitiveDefaultScanArgs() []string {
197203
args := make([]string, 0)
198204
cfg := config.GetInstance()
199205

200-
if cfg.ApiToken != "" {
201-
args = append(args, "--key", cfg.ApiToken)
206+
if cfg.GetApiToken() != "" {
207+
args = append(args, "--key", cfg.GetApiToken())
202208
}
203209

204-
if cfg.ApiUrl != "" {
205-
args = append(args, "--apiurl", fmt.Sprintf("%s/scan/direct", cfg.ApiUrl))
210+
if cfg.GetApiUrl() != "" {
211+
args = append(args, "--apiurl", fmt.Sprintf("%s/scan/direct", cfg.GetApiUrl()))
206212
}
207213

208214
return args
@@ -213,3 +219,29 @@ func (s *ScanServicePythonImpl) emitEvent(eventName string, data ...interface{})
213219
runtime.EventsEmit(s.ctx, eventName, data...)
214220
}
215221
}
222+
223+
func (s *ScanServicePythonImpl) GetScanArgs() []entities.ScanArgDef {
224+
return entities.ScanArguments
225+
}
226+
227+
func (s *ScanServicePythonImpl) maybeCreateOutputFolder(args []string) {
228+
outputPath := s.getOutputPathFromArgs(args)
229+
outputFolder := filepath.Dir(outputPath)
230+
if outputFolder != "" {
231+
if _, err := os.Stat(outputFolder); os.IsNotExist(err) {
232+
log.Info().Msgf("The provided output path does not exist. Creating it: %s", outputFolder)
233+
if err := os.MkdirAll(outputFolder, os.ModePerm); err != nil {
234+
log.Error().Err(err).Msgf("Failed to create output folder: %s", outputFolder)
235+
}
236+
}
237+
}
238+
}
239+
240+
func (s *ScanServicePythonImpl) getOutputPathFromArgs(args []string) string {
241+
for i := 0; i < len(args)-1; i++ {
242+
if args[i] == "--output" {
243+
return args[i+1]
244+
}
245+
}
246+
return ""
247+
}

0 commit comments

Comments
 (0)