mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Query reports/caching feature (#14460)
This PR includes all the frontend and backend work for #7766
This commit is contained in:
commit
b86b65db9b
134 changed files with 15436 additions and 1020 deletions
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
2
changes/13489-implement-api-changes
Normal file
2
changes/13489-implement-api-changes
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
* Add `GET /api/_version_/fleet/queries/{id}/report` API endpoint to retrieve the stored results of a given query.
|
||||
* Add `discard_data` field to API query endpoints.
|
||||
1
changes/14286-detail-query-overrides
Normal file
1
changes/14286-detail-query-overrides
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed a bug that would cause live queries to stall if a detail query override was set for a team.
|
||||
|
|
@ -804,6 +804,20 @@ func newCleanupsAndAggregationSchedule(
|
|||
return verifyDiskEncryptionKeys(ctx, logger, ds, config)
|
||||
},
|
||||
),
|
||||
schedule.WithJob("query_results_cleanup", func(ctx context.Context) error {
|
||||
config, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.ServerSettings.QueryReportsDisabled {
|
||||
if err = ds.CleanupGlobalDiscardQueryResults(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
|
||||
return s, nil
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -1204,9 +1203,9 @@ spec:
|
|||
// Apply queries.
|
||||
var appliedQueries []*fleet.Query
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
return nil, sql.ErrNoRows
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error {
|
||||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error {
|
||||
appliedQueries = queries
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1305,9 +1304,9 @@ func TestApplyQueries(t *testing.T) {
|
|||
|
||||
var appliedQueries []*fleet.Query
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
return nil, sql.ErrNoRows
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error {
|
||||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]bool) error {
|
||||
appliedQueries = queries
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,13 @@ import (
|
|||
"github.com/fatih/color"
|
||||
"github.com/fleetdm/fleet/v4/pkg/rawjson"
|
||||
"github.com/fleetdm/fleet/v4/pkg/secure"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/ghodss/yaml"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -462,6 +461,7 @@ func getQueriesCommand() *cli.Command {
|
|||
MinOsqueryVersion: query.MinOsqueryVersion,
|
||||
AutomationsEnabled: query.AutomationsEnabled,
|
||||
Logging: query.Logging,
|
||||
DiscardData: query.DiscardData,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("unable to print query: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1128,6 +1128,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1142,6 +1143,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 2
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1156,6 +1158,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: true
|
||||
description: some desc 4
|
||||
discard_data: false
|
||||
interval: 60
|
||||
logging: differential_ignore_removals
|
||||
min_osquery_version: 5.3.0
|
||||
|
|
@ -1165,9 +1168,9 @@ spec:
|
|||
query: select 4;
|
||||
team: ""
|
||||
`
|
||||
expectedJSONGlobal := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query4","description":"some desc 4","query":"select 4;","team":"","interval":60,"observer_can_run":true,"platform":"darwin,windows","min_osquery_version":"5.3.0","automations_enabled":true,"logging":"differential_ignore_removals"}}
|
||||
expectedJSONGlobal := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query4","description":"some desc 4","query":"select 4;","team":"","interval":60,"observer_can_run":true,"platform":"darwin,windows","min_osquery_version":"5.3.0","automations_enabled":true,"logging":"differential_ignore_removals","discard_data":false}}
|
||||
`
|
||||
|
||||
expectedTeam := `+--------+-------------+-----------+--------+----------------------------+
|
||||
|
|
@ -1191,6 +1194,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 3
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: snapshot
|
||||
min_osquery_version: 5.4.0
|
||||
|
|
@ -1200,7 +1204,7 @@ spec:
|
|||
query: select 3;
|
||||
team: Foobar
|
||||
`
|
||||
expectedJSONTeam := `{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"darwin","min_osquery_version":"5.4.0","automations_enabled":false,"logging":"snapshot"}}
|
||||
expectedJSONTeam := `{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"darwin","min_osquery_version":"5.4.0","automations_enabled":false,"logging":"snapshot","discard_data":false}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expectedGlobal, runAppForTest(t, []string{"get", "queries"}))
|
||||
|
|
@ -1276,6 +1280,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1285,7 +1290,7 @@ spec:
|
|||
query: select 1;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"globalQuery1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"globalQuery1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "globalQuery1"}))
|
||||
|
|
@ -1298,6 +1303,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: true
|
||||
description: some team desc
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: differential
|
||||
min_osquery_version: 5.2.0
|
||||
|
|
@ -1307,7 +1313,7 @@ spec:
|
|||
query: select 2;
|
||||
team: Foobar
|
||||
`
|
||||
expectedJson = `{"kind":"query","apiVersion":"v1","spec":{"name":"teamQuery1","description":"some team desc","query":"select 2;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"linux","min_osquery_version":"5.2.0","automations_enabled":true,"logging":"differential"}}
|
||||
expectedJson = `{"kind":"query","apiVersion":"v1","spec":{"name":"teamQuery1","description":"some team desc","query":"select 2;","team":"Foobar","interval":3600,"observer_can_run":true,"platform":"linux","min_osquery_version":"5.2.0","automations_enabled":true,"logging":"differential","discard_data":false}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--team", "1", "teamQuery1"}))
|
||||
|
|
@ -1432,6 +1438,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 2
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1441,7 +1448,7 @@ spec:
|
|||
query: select 2;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"}))
|
||||
|
|
@ -1509,6 +1516,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1523,6 +1531,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 2
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1537,6 +1546,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 3
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -1546,9 +1556,9 @@ spec:
|
|||
query: select 3;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":""}}
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;","team":"","interval":0,"observer_can_run":true,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;","team":"","interval":0,"observer_can_run":false,"platform":"","min_osquery_version":"","automations_enabled":false,"logging":"","discard_data":false}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"}))
|
||||
|
|
|
|||
25
cmd/fleetctl/testdata/convert_output.yml
vendored
25
cmd/fleetctl/testdata/convert_output.yml
vendored
|
|
@ -4,6 +4,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the list of application scheme/protocol-based IPC handlers.
|
||||
discard_data: false
|
||||
interval: 86400
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -18,6 +19,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the current disk encryption status for the target system.
|
||||
discard_data: false
|
||||
interval: 86400
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.5
|
||||
|
|
@ -32,6 +34,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the current disk encryption status for the target system.
|
||||
discard_data: false
|
||||
interval: 300
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.5
|
||||
|
|
@ -46,6 +49,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieve basic information about the physical disks of a system.
|
||||
discard_data: false
|
||||
interval: 86400
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -60,6 +64,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the current filters and chains per filter in the target system.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.5
|
||||
|
|
@ -76,6 +81,7 @@ spec:
|
|||
description:
|
||||
Retrieves all the daemons that will run in the start of the target
|
||||
OSX system.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.5
|
||||
|
|
@ -90,6 +96,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the list of listening ports.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -104,6 +111,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the list of listening ports.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -118,6 +126,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Lists the application bundle that owns a sandbox label.
|
||||
discard_data: false
|
||||
interval: 86400
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -132,6 +141,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: System resource usage limits.
|
||||
discard_data: false
|
||||
interval: 300
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -146,6 +156,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: System uptime.
|
||||
discard_data: false
|
||||
interval: 600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -160,6 +171,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: System uptime.
|
||||
discard_data: false
|
||||
interval: 600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -174,6 +186,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: System uptime.
|
||||
discard_data: false
|
||||
interval: 600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -188,6 +201,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: System uptime.
|
||||
discard_data: false
|
||||
interval: 600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -202,6 +216,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Lists the application bundle that owns a sandbox label.
|
||||
discard_data: false
|
||||
interval: 86400
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -216,6 +231,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -230,6 +246,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -244,6 +261,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -258,6 +276,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -272,6 +291,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -286,6 +306,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -300,6 +321,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
|
|
@ -314,6 +336,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: List of all user groups.
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -328,6 +351,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: Extracted information from Windows crash logs (Minidumps).
|
||||
discard_data: false
|
||||
interval: 3600
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
@ -344,6 +368,7 @@ spec:
|
|||
description:
|
||||
Triggers one-off YARA query for files at the specified path. Requires
|
||||
one of sig_group, sigfile, or sigrule.
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: 1.4.7
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false
|
||||
},
|
||||
|
|
@ -113,4 +114,4 @@
|
|||
},
|
||||
"scripts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: false
|
||||
live_query_disabled: false
|
||||
query_reports_disabled: false
|
||||
server_url: ""
|
||||
smtp_settings:
|
||||
authentication_method: ""
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"server_settings": {
|
||||
"server_url": "",
|
||||
"live_query_disabled": false,
|
||||
"query_reports_disabled": false,
|
||||
"enable_analytics": false,
|
||||
"deferred_save_host": false
|
||||
},
|
||||
|
|
@ -175,4 +176,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -88,6 +88,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: false
|
||||
live_query_disabled: false
|
||||
query_reports_disabled: false
|
||||
server_url: ""
|
||||
smtp_settings:
|
||||
authentication_method: ""
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: false
|
||||
live_query_disabled: false
|
||||
query_reports_disabled: false
|
||||
server_url: https://example.org
|
||||
smtp_settings:
|
||||
authentication_method: ""
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ spec:
|
|||
deferred_save_host: false
|
||||
enable_analytics: false
|
||||
live_query_disabled: false
|
||||
query_reports_disabled: false
|
||||
server_url: https://example.org
|
||||
smtp_settings:
|
||||
authentication_method: ""
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ func TestFleetctlUpgradePacks_EmptyPacks(t *testing.T) {
|
|||
outputFile := filepath.Join(tempDir, "output.yml")
|
||||
|
||||
// write some dummy data in the file, it should be overwritten
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0644)
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := runAppForTest(t, []string{"upgrade-packs", "-o", outputFile})
|
||||
|
|
@ -328,6 +328,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: false
|
||||
description: (converted from pack "p1", query "q1")
|
||||
discard_data: false
|
||||
interval: 0
|
||||
logging: snapshot
|
||||
min_osquery_version: ""
|
||||
|
|
@ -342,6 +343,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: true
|
||||
description: (converted from pack "p2", query "q2")
|
||||
discard_data: false
|
||||
interval: 90
|
||||
logging: differential
|
||||
min_osquery_version: ""
|
||||
|
|
@ -356,6 +358,7 @@ kind: query
|
|||
spec:
|
||||
automations_enabled: true
|
||||
description: (converted from pack "p2", query "q2")
|
||||
discard_data: false
|
||||
interval: 90
|
||||
logging: differential
|
||||
min_osquery_version: ""
|
||||
|
|
@ -371,7 +374,7 @@ spec:
|
|||
outputFile := filepath.Join(tempDir, "output.yml")
|
||||
|
||||
// write some dummy data in the file, it should be overwritten
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0644)
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
testUpgradePacksTimestamp = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
|
@ -394,7 +397,7 @@ func TestFleetctlUpgradePacks_NotAdmin(t *testing.T) {
|
|||
outputFile := filepath.Join(tempDir, "output.yml")
|
||||
|
||||
// write some dummy data in the file, it should NOT be overwritten
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0644)
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// first try without the required output file flag
|
||||
|
|
@ -422,7 +425,7 @@ func TestFleetctlUpgradePacks_NoPack(t *testing.T) {
|
|||
outputFile := filepath.Join(tempDir, "output.yml")
|
||||
|
||||
// write some dummy data in the file, it should NOT be overwritten
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0644)
|
||||
err := os.WriteFile(outputFile, []byte("dummy"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
got := runAppForTest(t, []string{"upgrade-packs", "-o", outputFile})
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ services:
|
|||
# Required for storage of Apple MDM installers.
|
||||
"--max_allowed_packet=536870912"
|
||||
]
|
||||
environment:
|
||||
&mysql-default-environment
|
||||
environment: &mysql-default-environment
|
||||
MYSQL_ROOT_PASSWORD: toor
|
||||
MYSQL_DATABASE: fleet
|
||||
MYSQL_USER: fleet
|
||||
|
|
@ -41,7 +40,7 @@ services:
|
|||
"--log_output=TABLE",
|
||||
"--log-queries-not-using-indexes",
|
||||
"--innodb-file-per-table=OFF",
|
||||
"--table-definition-cache=2048",
|
||||
"--table-definition-cache=8192",
|
||||
# These 3 keys run MySQL with GTID consistency enforced to avoid issues with production deployments that use it.
|
||||
"--enforce-gtid-consistency=ON",
|
||||
"--log-bin=bin.log",
|
||||
|
|
@ -142,3 +141,4 @@ volumes:
|
|||
mysql-persistent-volume:
|
||||
data-minio:
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
|||
live_query_disabled: false,
|
||||
enable_analytics: true,
|
||||
deferred_save_host: false,
|
||||
query_reports_disabled: false,
|
||||
},
|
||||
smtp_settings: {
|
||||
enable_smtp: false,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const DEFAULT_QUERY_MOCK: ISchedulableQuery = {
|
|||
author_name: "Test User",
|
||||
author_email: "test@example.com",
|
||||
observer_can_run: false,
|
||||
discard_data: false,
|
||||
interval: 300,
|
||||
packs: [],
|
||||
team_id: null,
|
||||
|
|
|
|||
331
frontend/__mocks__/queryReportMock.ts
Normal file
331
frontend/__mocks__/queryReportMock.ts
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import { IQueryReport } from "interfaces/query_report";
|
||||
|
||||
const DEFAULT_QUERY_REPORT_MOCK: IQueryReport = {
|
||||
query_id: 31,
|
||||
results: [
|
||||
{
|
||||
host_id: 1,
|
||||
host_name: "foo",
|
||||
last_fetched: "2021-01-19T17:08:31Z",
|
||||
columns: {
|
||||
model: "Razer Viper",
|
||||
vendor: "Razer",
|
||||
model_id: "0078",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 1,
|
||||
host_name: "foo",
|
||||
last_fetched: "2021-01-19T17:08:31Z",
|
||||
columns: {
|
||||
model: "USB Keyboard",
|
||||
vendor: "VIA Labs, Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Keyboard",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "YubiKey OTP+FIDO+CCID",
|
||||
vendor: "Yubico",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Lenovo USB Optical Mouse",
|
||||
vendor: "PixArt",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Lenovo Traditional USB Keyboard",
|
||||
vendor: "Lenovo",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Bose",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB-C Digital AV Multiport Adapter",
|
||||
vendor: "Apple, Inc.",
|
||||
model_id: "1460",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB-C Digital AV Multiport Adapter",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "1460",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 2,
|
||||
host_name: "bar",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Logitech Webcam C925e",
|
||||
model_id: "085b",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "Ambient Light Sensor",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 3,
|
||||
host_name: "zoo",
|
||||
last_fetched: "2022-04-09T17:20:00Z",
|
||||
columns: {
|
||||
model: "DELL Laser Mouse",
|
||||
model_id: "4d51",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "AppleUSBVHCIBCE Root Hub Simulation",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "QuickFire Rapid keyboard",
|
||||
vendor: "CM Storm",
|
||||
model_id: "0004",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "Lenovo USB Optical Mouse",
|
||||
vendor: "Lenovo",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 7,
|
||||
host_name: "Rachel's Magnificent Testing Computer of All Computers",
|
||||
last_fetched: "2023-09-21T19:03:30Z",
|
||||
columns: {
|
||||
model: "YubiKey FIDO+CCID",
|
||||
vendor: "Yubico",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 4,
|
||||
host_name: "car",
|
||||
last_fetched: "2023-01-14T12:40:30Z",
|
||||
columns: {
|
||||
model: "USB2.0 Hub",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "FaceTime HD Camera (Display)",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "1112",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple Internal Keyboard / Trackpad",
|
||||
model_id: "027e",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple Thunderbolt Display",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "9227",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "AppleUSBXHCI Root Hub Simulation",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "8007",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 8,
|
||||
host_name: "apple man",
|
||||
last_fetched: "2021-01-19T17:20:00Z",
|
||||
columns: {
|
||||
model: "Apple T2 Controller",
|
||||
vendor: "Apple Inc.",
|
||||
model_id: "8233",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "4-Port USB 2.0 Hub",
|
||||
vendor: "Generic",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB 10_100_1000 LAN",
|
||||
vendor: "Realtek",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB Mouse",
|
||||
vendor: "Razor",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 5,
|
||||
host_name: "choo",
|
||||
last_fetched: "2023-09-03T03:40:30Z",
|
||||
columns: {
|
||||
model: "USB Audio",
|
||||
vendor: "Apple, Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "Display Audio",
|
||||
vendor: "Apple Inc.",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "USB Reciever",
|
||||
vendor: "Logitech",
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: 6,
|
||||
host_name: "moo",
|
||||
last_fetched: "2023-09-20T07:02:34Z",
|
||||
columns: {
|
||||
model: "LG Monitor Controls",
|
||||
vendor: "LG Electronics Inc.",
|
||||
model_id: "9a39",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const createMockQueryReport = (
|
||||
overrides?: Partial<IQueryReport>
|
||||
): IQueryReport => {
|
||||
return { ...DEFAULT_QUERY_REPORT_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
export default createMockQueryReport;
|
||||
|
|
@ -20,6 +20,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = {
|
|||
author_name: "Test User",
|
||||
author_email: "test@example.com",
|
||||
observer_can_run: false,
|
||||
discard_data: false,
|
||||
packs: [],
|
||||
stats: {
|
||||
system_time_p50: 28.1053,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
&__container {
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
margin-bottom: 20px;
|
||||
min-height: 155px;
|
||||
max-width: none;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export interface IInfoBannerProps {
|
|||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
/** default light purple */
|
||||
color?: "yellow" | "grey";
|
||||
color?: "purple" | "purple-bold-border" | "yellow" | "grey";
|
||||
/** default 4px */
|
||||
borderRadius?: "large" | "xlarge";
|
||||
pageLevel?: boolean;
|
||||
|
|
@ -26,7 +26,7 @@ export interface IInfoBannerProps {
|
|||
const InfoBanner = ({
|
||||
children,
|
||||
className,
|
||||
color,
|
||||
color = "purple",
|
||||
borderRadius,
|
||||
pageLevel,
|
||||
cta,
|
||||
|
|
@ -36,6 +36,7 @@ const InfoBanner = ({
|
|||
}: IInfoBannerProps): JSX.Element => {
|
||||
const wrapperClasses = classNames(
|
||||
baseClass,
|
||||
`${baseClass}__${color}`,
|
||||
{
|
||||
[`${baseClass}__${color}`]: !!color,
|
||||
[`${baseClass}__border-radius-${borderRadius}`]: !!borderRadius,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,19 @@
|
|||
padding: $pad-medium;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $ui-vibrant-blue-50;
|
||||
background-color: $ui-vibrant-blue-10;
|
||||
font-size: $x-small;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
|
||||
&__purple {
|
||||
background-color: $ui-vibrant-blue-10;
|
||||
}
|
||||
|
||||
&__purple-bold-border {
|
||||
background-color: $ui-vibrant-blue-10;
|
||||
border-color: $core-vibrant-blue;
|
||||
}
|
||||
|
||||
&__yellow {
|
||||
background-color: $ui-yellow-banner;
|
||||
border-color: $ui-yellow-banner-outline;
|
||||
|
|
@ -35,6 +43,9 @@
|
|||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
p {
|
||||
margin: $pad-small 0 0 0;
|
||||
}
|
||||
|
|
@ -46,6 +57,8 @@
|
|||
color: $core-fleet-black;
|
||||
text-align: right;
|
||||
gap: $pad-small;
|
||||
min-width: max-content;
|
||||
margin-left: $pad-small;
|
||||
|
||||
button {
|
||||
margin-left: $pad-small;
|
||||
|
|
@ -65,4 +78,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
ISelectLabel,
|
||||
ISelectTeam,
|
||||
ISelectTargetsEntity,
|
||||
ISelectedTargets,
|
||||
ISelectedTargetsForApi,
|
||||
} from "interfaces/target";
|
||||
import { ITeam } from "interfaces/team";
|
||||
|
||||
|
|
@ -48,7 +48,9 @@ interface ISelectTargetsProps {
|
|||
targetedTeams: ITeam[];
|
||||
goToQueryEditor: () => void;
|
||||
goToRunQuery: () => void;
|
||||
setSelectedTargets: React.Dispatch<React.SetStateAction<ITarget[]>>;
|
||||
setSelectedTargets: // TODO: Refactor policy targets to streamline selectedTargets/selectedTargetsByType
|
||||
| React.Dispatch<React.SetStateAction<ITarget[]>> // Used for policies page level useState hook
|
||||
| ((value: ITarget[]) => void); // Used for queries app level QueryContext
|
||||
setTargetedHosts: React.Dispatch<React.SetStateAction<IHost[]>>;
|
||||
setTargetedLabels: React.Dispatch<React.SetStateAction<ILabel[]>>;
|
||||
setTargetedTeams: React.Dispatch<React.SetStateAction<ITeam[]>>;
|
||||
|
|
@ -65,7 +67,7 @@ interface ITargetsQueryKey {
|
|||
scope: string;
|
||||
query_id?: number | null;
|
||||
query?: string | null;
|
||||
selected?: ISelectedTargets | null;
|
||||
selected?: ISelectedTargetsForApi | null;
|
||||
}
|
||||
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
|
|
@ -379,12 +381,22 @@ const SelectTargets = ({
|
|||
}
|
||||
|
||||
const { targets_count: total, targets_online: online } = counts;
|
||||
const onlinePercentage = total > 0 ? Math.round((online / total) * 100) : 0;
|
||||
const onlinePercentage = () => {
|
||||
if (total === 0) {
|
||||
return 0;
|
||||
}
|
||||
// If at least 1 host is online, displays <1% instead of 0%
|
||||
const roundPercentage =
|
||||
Math.round((online / total) * 100) === 0
|
||||
? "<1"
|
||||
: Math.round((online / total) * 100) === 0;
|
||||
return roundPercentage;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{total}</span> host{total > 1 ? `s` : ``} targeted (
|
||||
{onlinePercentage}
|
||||
{onlinePercentage()}
|
||||
%
|
||||
<TooltipWrapper
|
||||
tipContent={`Hosts are online if they<br /> have recently checked <br />into Fleet.`}
|
||||
|
|
|
|||
|
|
@ -79,4 +79,7 @@
|
|||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.input-icon-field__icon {
|
||||
top: 34px; // Override styling to include label header
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ const DefaultColumnFilter = ({
|
|||
}: FilterProps<TableInstance>): JSX.Element => {
|
||||
const { setFilter } = column;
|
||||
|
||||
// Remove last_fetched filter per design as it is confusing to filter by a non-displayed date-string
|
||||
if (column.id === "last_fetched") {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"filter-cell"}>
|
||||
<SearchField
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import * as DOMPurify from "dompurify";
|
|||
interface ITooltipWrapperProps {
|
||||
children: string | JSX.Element;
|
||||
tipContent: string;
|
||||
/** Default: bottom */
|
||||
position?: "top" | "bottom";
|
||||
isDelayed?: boolean;
|
||||
className?: string;
|
||||
|
|
|
|||
|
|
@ -341,9 +341,7 @@ $base-class: "button";
|
|||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.5);
|
||||
cursor: default;
|
||||
@include disabled;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React from "react";
|
||||
import { COLORS, Colors } from "styles/var/colors";
|
||||
import { IconSizes, ICON_SIZES } from "styles/var/icon_sizes";
|
||||
|
||||
interface IChevronProps {
|
||||
color?: Colors;
|
||||
/** Default direction "down" */
|
||||
direction?: "up" | "down" | "left" | "right";
|
||||
size: IconSizes;
|
||||
}
|
||||
|
||||
const SVG_PATH = {
|
||||
|
|
@ -17,11 +19,12 @@ const SVG_PATH = {
|
|||
const Chevron = ({
|
||||
color = "core-fleet-black",
|
||||
direction = "down",
|
||||
size = "medium",
|
||||
}: IChevronProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
width={ICON_SIZES[size]}
|
||||
height={ICON_SIZES[size]}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
|
|
|
|||
254
frontend/components/icons/CollectingResults.tsx
Normal file
254
frontend/components/icons/CollectingResults.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import React from "react";
|
||||
|
||||
const CollectingResults = () => {
|
||||
return (
|
||||
<svg
|
||||
width="240"
|
||||
height="126"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 240 126"
|
||||
>
|
||||
<g clipPath="url(#a)">
|
||||
<path
|
||||
d="M109.214 120.128c26.769-13.44 35.513-50.12 19.53-81.927C112.761 6.394 78.104-8.496 51.335 4.945c-26.768 13.44-35.512 50.12-19.53 81.927 15.984 31.807 50.64 46.697 77.409 33.256Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="M123.697 16.088a14.505 14.505 0 0 1 10.124 4.55 14.49 14.49 0 0 1 3.94 10.374 1.478 1.478 0 0 0 2.475 1.09c.283-.26.454-.62.475-1.004a17.439 17.439 0 0 0-7.27-14.696 17.454 17.454 0 0 0-9.657-3.26 1.472 1.472 0 0 0-1.388.87 1.466 1.466 0 0 0-.034 1.127 1.475 1.475 0 0 0 1.335.95Zm22.334 16.65a1.48 1.48 0 0 0 1.055-.4 1.466 1.466 0 0 0 .463-1.03 24.47 24.47 0 0 0-.309-4.65 24.218 24.218 0 0 0-3.296-8.828 24.288 24.288 0 0 0-19.959-11.517 1.477 1.477 0 0 0-1.094 2.474c.26.283.621.454 1.005.475a21.35 21.35 0 0 1 14.901 6.693 21.332 21.332 0 0 1 5.8 15.265 1.469 1.469 0 0 0 .87 1.39c.177.08.368.123.562.128h.002Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="M185.292 85.93v6.313h-19.379v-6.314a.937.937 0 0 1 .936-.935h17.507a.935.935 0 0 1 .936.935Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="M196.958 114.897V89.31a1.87 1.87 0 0 0-1.871-1.87h-65.532a1.87 1.87 0 0 0-1.871 1.87v25.587a1.87 1.87 0 0 0 1.871 1.871h65.532a1.87 1.87 0 0 0 1.871-1.871Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="M163.092 113.824v-8.418a.468.468 0 0 0-.468-.468h-3.743a.468.468 0 0 0-.468.468v8.418c0 .258.21.468.468.468h3.743c.258 0 .468-.21.468-.468Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="M155.318 112.587V91.621c0-1.147-.93-2.077-2.077-2.077h-21.375c-1.147 0-2.077.93-2.077 2.077v20.966c0 1.147.93 2.076 2.077 2.076h21.375a2.076 2.076 0 0 0 2.077-2.076Z"
|
||||
fill="#E6E6F7"
|
||||
/>
|
||||
<path
|
||||
d="M181.722 112.966c6.002 0 10.867-4.863 10.867-10.862s-4.865-10.862-10.867-10.862c-6.001 0-10.866 4.863-10.866 10.862s4.865 10.862 10.866 10.862Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M186.333 106.556v-8.907a.935.935 0 0 0-1.871 0v8.907a.936.936 0 1 0 1.871 0ZM182.59 106.556v-8.907a.935.935 0 0 0-1.871 0v8.907a.936.936 0 1 0 1.871 0ZM178.847 106.556v-8.907a.935.935 0 0 0-1.871 0v8.907a.936.936 0 1 0 1.871 0Z"
|
||||
fill="#B0B2E7"
|
||||
/>
|
||||
<path
|
||||
d="M238.012 116.625a1.578 1.578 0 0 1-.349-.035c-14.607-2.944-26.523-4.641-44.199-4.641a1.754 1.754 0 1 1 0-3.508c17.975 0 30.074 1.728 44.901 4.712a1.755 1.755 0 0 1 1.397 1.893 1.76 1.76 0 0 1-1.743 1.579h-.007Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m78.157 120.042-50.855 4.677V74.677l50.855-2.105v47.47Z"
|
||||
fill="#E6E6F7"
|
||||
/>
|
||||
<path
|
||||
d="m45.776 109.065 13.945-.905-1.448-34.76 6.882-.36 2.897 47.792-40.836 3.873 18.56-15.64Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m38.328 123-1.048-.936c.025-.011 2.793-1.218 5.418-4.842 4.405-6.08 5.708-14.145 5.455-19.877-.253-5.731-3.626-12.201-9.357-17.5-3.38-3.145-11.098-5.08-11.131-5.086-.033-.007 4.74-.286 6.248-.363 1.362.622 4.595 2.285 6.429 3.975 6.082 5.613 8.95 12.862 9.215 18.888.264 6.026-.41 14.498-5.11 20.917-1.92 2.626-3.897 4.061-5.02 4.735l-1.1.089Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 124.705c11.505 0 20.832-11.192 20.832-24.998 0-13.806-9.327-24.997-20.832-24.997S6.384 85.9 6.384 99.707s9.327 24.998 20.832 24.998Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 122.81c10.633 0 19.253-10.343 19.253-23.103 0-12.76-8.62-23.103-19.253-23.103-10.633 0-19.253 10.343-19.253 23.103 0 12.76 8.62 23.103 19.253 23.103Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 120.841c9.726 0 17.61-9.462 17.61-21.134s-7.884-21.134-17.61-21.134c-9.726 0-17.61 9.462-17.61 21.134s7.884 21.134 17.61 21.134Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 119.078c8.916 0 16.144-8.672 16.144-19.37 0-10.7-7.228-19.372-16.144-19.372-8.916 0-16.144 8.673-16.144 19.371 0 10.699 7.228 19.371 16.144 19.371Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 116.78c7.858 0 14.228-7.644 14.228-17.073s-6.37-17.073-14.228-17.073-14.228 7.644-14.228 17.073 6.37 17.073 14.228 17.073Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 101.505c.828 0 1.5-.805 1.5-1.798s-.672-1.798-1.5-1.798-1.5.805-1.5 1.798.672 1.798 1.5 1.798Z"
|
||||
fill="#B0B2E7"
|
||||
/>
|
||||
<path
|
||||
d="m45.814 123-.37-.884c.026-.012 2.793-1.218 5.416-4.843 4.407-6.08 5.032-14.208 4.779-19.947-.253-5.738-1.755-13.724-7.486-19.022-3.387-3.136-7.797-4.158-7.83-4.165-.032-.007 2.293-.091 3.802-.166a18.424 18.424 0 0 1 4.982 3.302c6.083 5.612 7.666 13.974 7.93 19.99.265 6.017-.409 14.499-5.108 20.918-1.92 2.626-3.898 4.061-5.02 4.735l-1.095.082Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m51.615 122.462-.234-.578c.021-.011 2.2-1.218 4.267-4.842 3.472-6.08 3.963-14.209 3.764-19.947-.199-5.738-1.382-13.724-5.892-19.023a14.248 14.248 0 0 0-6.17-4.165s1.807-.09 2.995-.166a14.737 14.737 0 0 1 3.928 3.302c4.786 5.612 6.038 13.974 6.246 19.991.208 6.017-.323 14.498-4.024 20.917-1.513 2.626-2.9 3.742-3.785 4.42-.416.04-1.095.091-1.095.091Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m23.005 99.707.9-.327c-5.953 0-11.68 5.144-11.68 11.102v10.289a3.491 3.491 0 0 1-3.495 3.492H0"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M25.11 99.707h.789c-5.954 0-11.568 4.834-11.568 10.785v10.289a3.486 3.486 0 0 1-2.158 3.226 3.505 3.505 0 0 1-1.337.265h-8.73"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M27.216 99.707a10.781 10.781 0 0 0-10.78 10.775v10.289a3.493 3.493 0 0 1-3.495 3.492h-8.73"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="m70.225 65.3-5.547.19-.089-20.897 5.547-.119.089 20.826Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m83.854 125.404-4.396.402a14.325 14.325 0 0 1-14.427-8.467 14.309 14.309 0 0 1-1.22-5.783V65.894a.702.702 0 0 1 .685-.702l19.358-.451v60.663Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m131.515 64.13-61.924 1.125-.09-21.363 61.923.816.091 19.423Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m82.5 56.283-5.536.072a1.997 1.997 0 0 1-2.02-1.99v-.92a1.993 1.993 0 0 1 2.006-2l5.533.033a1.996 1.996 0 0 1 1.984 1.987v.816a1.992 1.992 0 0 1-1.968 2.002Z"
|
||||
fill="#B0B2E7"
|
||||
/>
|
||||
<path
|
||||
d="m121.59 121.333-39.245 4.396a11.72 11.72 0 0 1-9.11-2.908 11.71 11.71 0 0 1-3.904-8.726V65.134l61.951-1.155a.705.705 0 0 1 .659.43.701.701 0 0 1 .054.271v45.033a11.698 11.698 0 0 1-10.405 11.62Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m81.921 44.02-17.22.63a.701.701 0 0 1-.727-.702l-.037-10.338c-.029-7.633 6.126-13.616 13.762-13.668l4.134-.026-1.736 3.508-5.977 5.081 2.987 12.038 4.814 3.478Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="M74.09 22.229v3.04l39.38 1.183a6.519 6.519 0 0 1 6.274 6.618l-.047 2.787a5.477 5.477 0 0 1-5.528 5.378l-40.08-.194v1.085h52.883v-16.79L74.089 22.23Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m121.976 21.855-40.195-1.948a11.89 11.89 0 0 0-11.5 7.186 11.876 11.876 0 0 0-.962 4.679v12.627l61.958.468a.702.702 0 0 0 .702-.702V32.36a10.523 10.523 0 0 0-10.003-10.504Zm4.559 12.26a7.268 7.268 0 0 1-7.306 7.275l-34.856-.162a8.636 8.636 0 0 1-8.595-8.628v-1.523a6.928 6.928 0 0 1 4.455-6.472 6.938 6.938 0 0 1 2.73-.454l37.045 1.373a6.78 6.78 0 0 1 6.529 6.765l-.002 1.826Z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m126.441 111.109-46.035 5.685a6.245 6.245 0 0 1-4.893-1.518 6.23 6.23 0 0 1-2.102-4.67V76.117h53.03v34.992Z"
|
||||
fill="#D7DAF5"
|
||||
/>
|
||||
<path
|
||||
d="m126.182 111.074-45.325 4.859a5.788 5.788 0 0 1-5.855-3.277 5.79 5.79 0 0 1-.555-2.302l-1.03-33.327a5.844 5.844 0 0 1 5.653-6.035l46.059-1.705a4.958 4.958 0 0 1 5.147 4.786l1.052 31.115a5.712 5.712 0 0 1-5.146 5.886Z"
|
||||
fill="#fff"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="m125.992 97.92-46.454 3.821a3.59 3.59 0 0 1-2.682-.897 3.587 3.587 0 0 1-1.197-2.561l-.688-21.565a4.054 4.054 0 0 1 3.9-4.18l46.532-1.806a3.664 3.664 0 0 1 3.482 2.158c.198.442.307.919.319 1.403l.508 19.477a4.05 4.05 0 0 1-3.72 4.15Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="m126.189 96.45-46.506 3.809a2.503 2.503 0 0 1-1.881-.63 2.51 2.51 0 0 1-.838-1.797L76.42 79.11a2.68 2.68 0 0 1 2.574-2.761l46.929-1.913a2.005 2.005 0 0 1 2.091 1.964l.384 17.604a2.404 2.404 0 0 1-2.208 2.446Z"
|
||||
fill="#F1F1FD"
|
||||
/>
|
||||
<path
|
||||
d="M99.03 53.399v1.328h-3.676V53.4h3.677Zm-3.1-2.845v7.166h-1.728v-7.166h1.728Zm4.262 0v7.166H98.47v-7.166h1.722Zm6.634 5.837v1.329h-3.819v-1.329h3.819Zm-3.204-5.837v7.166h-1.727v-7.166h1.727Zm2.712 2.845v1.284h-3.327V53.4h3.327Zm.507-2.845v1.334h-3.834v-1.334h3.834Zm6.034 5.837v1.329h-3.623v-1.329h3.623Zm-3.008-5.837v7.166h-1.727v-7.166h1.727Zm9.076 5.837v1.329h-3.623v-1.329h3.623Zm-3.007-5.837v7.166h-1.728v-7.166h1.728Zm10.089 3.44v.29c0 .549-.077 1.04-.232 1.477-.15.433-.365.804-.644 1.112-.279.306-.609.54-.99.704-.38.161-.8.241-1.26.241-.462 0-.886-.08-1.269-.24a2.915 2.915 0 0 1-.995-.705 3.266 3.266 0 0 1-.649-1.112 4.499 4.499 0 0 1-.227-1.477v-.29c0-.548.076-1.04.227-1.476a3.26 3.26 0 0 1 .644-1.113c.279-.308.609-.543.99-.704a3.198 3.198 0 0 1 1.27-.246c.459 0 .879.082 1.26.246.383.161.715.396.994.704.279.305.495.676.649 1.113.155.436.232.928.232 1.476Zm-1.752.29v-.3a4.19 4.19 0 0 0-.094-.935 2.071 2.071 0 0 0-.266-.684 1.18 1.18 0 0 0-1.023-.556 1.143 1.143 0 0 0-1.029.556 2.138 2.138 0 0 0-.261.684c-.056.27-.084.58-.084.935v.3c0 .352.028.663.084.936.059.269.146.497.261.684.118.187.262.328.433.423.174.095.376.143.605.143.22 0 .415-.048.586-.143.171-.095.313-.236.428-.423.118-.187.207-.415.266-.684.062-.273.094-.584.094-.935ZM95.558 86.67a2.264 2.264 0 1 0 0-4.527 2.264 2.264 0 0 0 0 4.528ZM111.699 85.735a2.263 2.263 0 1 0 0-4.527 2.264 2.264 0 1 0 0 4.527Z"
|
||||
fill="#B0B2E7"
|
||||
/>
|
||||
<path
|
||||
d="m101.375 89.205 5.797-.234"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.887"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M165.913 87.386v-1.457a.937.937 0 0 1 .936-.935h17.507a.935.935 0 0 1 .936.935v1.548"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M131.894 87.44h63.193a1.871 1.871 0 0 1 1.871 1.87v25.585a1.871 1.871 0 0 1-1.871 1.871h-65.338"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M131.864 89.544h21.274a2.182 2.182 0 0 1 2.18 2.18v20.753a2.178 2.178 0 0 1-2.18 2.179h-22.196M181.722 112.966c6.002 0 10.867-4.863 10.867-10.862s-4.865-10.862-10.867-10.862c-6.001 0-10.866 4.863-10.866 10.862s4.865 10.862 10.866 10.862ZM196.956 108.464c16.092.213 27.558 1.904 41.406 4.689a1.755 1.755 0 0 1 .83 3.018 1.75 1.75 0 0 1-1.524.419c-13.629-2.743-24.917-4.408-40.728-4.618M29.296 124.59l38.033-3.515M69.01 43.747a.774.774 0 0 0 .765.783l61.172.398a.98.98 0 0 0 .992-.966c0-1.87 0-6.262-.045-12.188-.03-5.144-3.534-7.716-6.012-8.902-1.201-.56-2.5-.878-3.825-.936-6.187-.299-30.998-1.508-40.795-2.06a8.657 8.657 0 0 0-1.907.103c-10.125 1.654-10.11 12.244-10.11 12.244l-.235 11.524Z"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M79.914 19.904s-11.837-.603-15.194 9.31a14.378 14.378 0 0 0-.723 4.517c-.03 4.488-.014 8.091 0 9.822a1.103 1.103 0 0 0 1.13 1.11l4.494-.095M69.736 44.654v20.33M64.844 44.654v20.419M131.26 45.16v18.737M131.894 64.816v45.531a10.753 10.753 0 0 1-9.546 10.696l-40.558 4.599a11.371 11.371 0 0 1-8.856-2.815 11.36 11.36 0 0 1-3.795-8.479v-48.55a.828.828 0 0 1 .814-.828l61.073-1.005a.849.849 0 0 1 .868.85Z"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M81.152 125.691c-10.595.875-17.393-5.96-17.393-14.03V65.999a.783.783 0 0 1 .744-.781l5.542-.264M26.035 74.721l37.55-1.328M27.216 124.704c11.505 0 20.832-11.191 20.832-24.997S38.72 74.71 27.216 74.71 6.384 85.9 6.384 99.707s9.327 24.997 20.832 24.997ZM119.606 41.357H84.191a8.169 8.169 0 0 1-8.162-8.166v-2.754a6.473 6.473 0 0 1 6.725-6.47l37.385 1.447a6.782 6.782 0 0 1 6.527 6.795v2.105a7.04 7.04 0 0 1-4.359 6.51 7.055 7.055 0 0 1-2.701.533Z"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="m78.649 25.278 34.912 1.11a6.426 6.426 0 0 1 6.227 6.428v3.195a5.352 5.352 0 0 1-5.303 5.353M163.092 113.824v-8.418a.468.468 0 0 0-.468-.468h-3.743a.468.468 0 0 0-.468.468v8.418c0 .258.21.468.468.468h3.743c.258 0 .468-.21.468-.468Z"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth=".472"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M147.107 117.091c.261 2.609-1.423 4.289-6.054 4.289M76.927 53.472v8.797a4.977 4.977 0 0 1-4.98 4.976h-7.17c-3.4 0-8.497 2.757-8.497 6.157"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M79.734 53.472v8.797a4.977 4.977 0 0 1-4.98 4.976h-7.17c-3.4 0-8.497 2.757-8.497 6.157"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="M82.307 53.472v8.797a4.977 4.977 0 0 1-4.98 4.976h-7.17c-3.4 0-8.497 2.757-8.497 6.157"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
<path
|
||||
d="m140.211 121.8-4.319-.002a9.98 9.98 0 0 1-9.984-9.98v-3.566a3.564 3.564 0 0 0-3.56-3.562h-.234"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M157.707 86.156h-.708.708Zm-7.198-7.195v.707-.707Zm-28.391 29.291-.707-.001v.001h.707Zm-3.56-4.269a.707.707 0 1 0 0 1.415v-1.415Zm39.856-16.604v-1.223h-1.415v1.223h1.415Zm0-1.223a7.903 7.903 0 0 0-2.315-5.588l-1.001 1a6.487 6.487 0 0 1 1.901 4.588h1.415Zm-2.315-5.588a7.908 7.908 0 0 0-5.59-2.315v1.415c1.721 0 3.372.684 4.589 1.9l1.001-1Zm-5.59-2.315a7.905 7.905 0 0 0-5.59 2.315l1 1a6.492 6.492 0 0 1 4.59-1.9v-1.415Zm-5.59 2.315a7.9 7.9 0 0 0-2.316 5.588h1.415c0-1.72.684-3.37 1.901-4.587l-1-1.001Zm-2.316 5.588v30.6h1.415v-30.6h-1.415Zm0 30.6a5.033 5.033 0 0 1-1.476 3.561l1.001 1.001a6.452 6.452 0 0 0 1.89-4.562h-1.415Zm-1.476 3.561a5.039 5.039 0 0 1-3.562 1.475v1.415c1.711 0 3.353-.68 4.563-1.889l-1.001-1.001Zm-3.562 1.475h-5.462v1.415h5.462v-1.415Zm-5.462 0a9.279 9.279 0 0 1-6.56-2.716l-1 1.001a10.694 10.694 0 0 0 7.56 3.13v-1.415Zm-6.56-2.716a9.27 9.27 0 0 1-2.717-6.557h-1.415c0 2.835 1.126 5.553 3.132 7.558l1-1.001Zm-2.717-6.557v-4.267h-1.415v4.267h1.415Zm0-4.267c0-.561-.11-1.116-.324-1.634l-1.308.541c.144.347.217.717.217 1.092l1.415.001Zm-.324-1.634a4.288 4.288 0 0 0-.925-1.385l-1.001 1.001c.265.265.475.579.618.925l1.308-.541Zm-.925-1.385a4.272 4.272 0 0 0-1.385-.925l-.542 1.307c.346.143.661.354.926.619l1.001-1.001Zm-1.385-.925a4.26 4.26 0 0 0-1.634-.325v1.415c.375 0 .746.074 1.092.217l.542-1.307Z"
|
||||
fill="#B0B2E7"
|
||||
/>
|
||||
<path
|
||||
d="M153.543 87.379v-1.223a7.194 7.194 0 0 0-7.199-7.195h-.329c-4.492.203-7.57 4.642-6.38 8.977l7.134 25.939c.22.798.331 1.623.331 2.45v.846"
|
||||
stroke="#B0B2E7"
|
||||
strokeWidth="1.415"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h240v126H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectingResults;
|
||||
|
|
@ -4,6 +4,7 @@ import ArrowInternalLink from "./ArrowInternalLink";
|
|||
import CalendarCheck from "./CalendarCheck";
|
||||
import Check from "./Check";
|
||||
import Chevron from "./Chevron";
|
||||
import CollectingResults from "./CollectingResults";
|
||||
import Columns from "./Columns";
|
||||
import CriticalPolicy from "./CriticalPolicy";
|
||||
import Disable from "./Disable";
|
||||
|
|
@ -84,6 +85,7 @@ export const ICON_MAP = {
|
|||
"calendar-check": CalendarCheck,
|
||||
chevron: Chevron,
|
||||
check: Check,
|
||||
"collecting-results": CollectingResults,
|
||||
columns: Columns,
|
||||
"critical-policy": CriticalPolicy,
|
||||
disable: Disable,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import React from "react";
|
||||
|
||||
import PhoneHome from "../../../../../assets/images/phone-home.svg";
|
||||
import EmptyTable from "components/EmptyTable/EmptyTable";
|
||||
|
||||
const baseClass = "awaiting-results";
|
||||
|
||||
const AwaitingResults = () => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<img src={PhoneHome} alt="awaiting results" />
|
||||
<span className={`${baseClass}__title`}>Phoning home...</span>
|
||||
<p className={`${baseClass}__description`}>
|
||||
There are currently no results to your query. Please wait while we talk
|
||||
to more hosts.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyTable
|
||||
iconName="collecting-results"
|
||||
header="Phoning home..."
|
||||
info=" There are currently no results to your query. Please wait while we talk
|
||||
to more hosts."
|
||||
className={baseClass}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,19 +5,4 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import { DEFAULT_QUERY } from "utilities/constants";
|
|||
import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
import { QueryLoggingOption } from "interfaces/schedulable_query";
|
||||
import {
|
||||
DEFAULT_TARGETS,
|
||||
DEFAULT_TARGETS_BY_TYPE,
|
||||
ISelectedTargetsByType,
|
||||
ITarget,
|
||||
} from "interfaces/target";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
|
|
@ -22,6 +28,9 @@ type InitialStateType = {
|
|||
lastEditedQueryPlatforms: SelectedPlatformString;
|
||||
lastEditedQueryMinOsqueryVersion: string;
|
||||
lastEditedQueryLoggingType: QueryLoggingOption;
|
||||
lastEditedQueryDiscardData: boolean;
|
||||
selectedQueryTargets: ITarget[]; // Mimicks old selectedQueryTargets still used for policies for SelectTargets.tsx and running a live query
|
||||
selectedQueryTargetsByType: ISelectedTargetsByType; // New format by type for cleaner app wide state
|
||||
setLastEditedQueryId: (value: number | null) => void;
|
||||
setLastEditedQueryName: (value: string) => void;
|
||||
setLastEditedQueryDescription: (value: string) => void;
|
||||
|
|
@ -31,7 +40,10 @@ type InitialStateType = {
|
|||
setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void;
|
||||
setLastEditedQueryMinOsqueryVersion: (value: string) => void;
|
||||
setLastEditedQueryLoggingType: (value: string) => void;
|
||||
setLastEditedQueryDiscardData: (value: boolean) => void;
|
||||
setSelectedOsqueryTable: (tableName: string) => void;
|
||||
setSelectedQueryTargets: (value: ITarget[]) => void;
|
||||
setSelectedQueryTargetsByType: (value: ISelectedTargetsByType) => void;
|
||||
};
|
||||
|
||||
export type IQueryContext = InitialStateType;
|
||||
|
|
@ -48,6 +60,9 @@ const initialState = {
|
|||
lastEditedQueryPlatforms: DEFAULT_QUERY.platform,
|
||||
lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version,
|
||||
lastEditedQueryLoggingType: DEFAULT_QUERY.logging,
|
||||
lastEditedQueryDiscardData: DEFAULT_QUERY.discard_data,
|
||||
selectedQueryTargets: DEFAULT_TARGETS,
|
||||
selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE,
|
||||
setLastEditedQueryId: () => null,
|
||||
setLastEditedQueryName: () => null,
|
||||
setLastEditedQueryDescription: () => null,
|
||||
|
|
@ -57,12 +72,17 @@ const initialState = {
|
|||
setLastEditedQueryPlatforms: () => null,
|
||||
setLastEditedQueryMinOsqueryVersion: () => null,
|
||||
setLastEditedQueryLoggingType: () => null,
|
||||
setLastEditedQueryDiscardData: () => null,
|
||||
setSelectedOsqueryTable: () => null,
|
||||
setSelectedQueryTargets: () => null,
|
||||
setSelectedQueryTargetsByType: () => null,
|
||||
};
|
||||
|
||||
const actions = {
|
||||
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
|
||||
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
|
||||
SET_SELECTED_QUERY_TARGETS: "SET_SELECTED_QUERY_TARGETS",
|
||||
SET_SELECTED_QUERY_TARGETS_BY_TYPE: "SET_SELECTED_QUERY_TARGETS_BY_TYPE",
|
||||
} as const;
|
||||
|
||||
const reducer = (state: InitialStateType, action: any) => {
|
||||
|
|
@ -113,6 +133,26 @@ const reducer = (state: InitialStateType, action: any) => {
|
|||
typeof action.lastEditedQueryLoggingType === "undefined"
|
||||
? state.lastEditedQueryLoggingType
|
||||
: action.lastEditedQueryLoggingType,
|
||||
lastEditedQueryDiscardData:
|
||||
typeof action.lastEditedQueryDiscardData === "undefined"
|
||||
? state.lastEditedQueryDiscardData
|
||||
: action.lastEditedQueryDiscardData,
|
||||
};
|
||||
case actions.SET_SELECTED_QUERY_TARGETS:
|
||||
return {
|
||||
...state,
|
||||
selectedQueryTargets:
|
||||
typeof action.selectedQueryTargets === "undefined"
|
||||
? state.selectedQueryTargets
|
||||
: action.selectedQueryTargets,
|
||||
};
|
||||
case actions.SET_SELECTED_QUERY_TARGETS_BY_TYPE:
|
||||
return {
|
||||
...state,
|
||||
selectedQueryTargetsByType:
|
||||
typeof action.selectedQueryTargetsByType === "undefined"
|
||||
? state.selectedQueryTargetsByType
|
||||
: action.selectedQueryTargetsByType,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
|
@ -135,6 +175,9 @@ const QueryProvider = ({ children }: Props) => {
|
|||
lastEditedQueryPlatforms: state.lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
|
||||
lastEditedQueryDiscardData: state.lastEditedQueryDiscardData,
|
||||
selectedQueryTargets: state.selectedQueryTargets,
|
||||
selectedQueryTargetsByType: state.selectedQueryTargetsByType,
|
||||
setLastEditedQueryId: (lastEditedQueryId: number | null) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
|
|
@ -193,6 +236,26 @@ const QueryProvider = ({ children }: Props) => {
|
|||
lastEditedQueryLoggingType,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryDiscardData: (lastEditedQueryDiscardData: boolean) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryDiscardData,
|
||||
});
|
||||
},
|
||||
setSelectedQueryTargets: (selectedQueryTargets: ITarget[]) => {
|
||||
dispatch({
|
||||
type: actions.SET_SELECTED_QUERY_TARGETS,
|
||||
selectedQueryTargets,
|
||||
});
|
||||
},
|
||||
setSelectedQueryTargetsByType: (
|
||||
selectedQueryTargetsByType: ISelectedTargetsByType
|
||||
) => {
|
||||
dispatch({
|
||||
type: actions.SET_SELECTED_QUERY_TARGETS_BY_TYPE,
|
||||
selectedQueryTargetsByType,
|
||||
});
|
||||
},
|
||||
setSelectedOsqueryTable: (tableName: string) => {
|
||||
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { filter, uniqueId } from "lodash";
|
|||
import { IHost } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { ISelectedTargets } from "interfaces/target";
|
||||
import { ISelectedTargetsForApi } from "interfaces/target";
|
||||
import targetsAPI from "services/entities/targets";
|
||||
|
||||
export interface ITargetsLabels {
|
||||
|
|
@ -25,7 +25,7 @@ export interface ITargetsQueryKey {
|
|||
scope: string;
|
||||
query: string;
|
||||
queryId: number | null;
|
||||
selected: ISelectedTargets;
|
||||
selected: ISelectedTargetsForApi;
|
||||
includeLabels: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export interface IConfig {
|
|||
live_query_disabled: boolean;
|
||||
enable_analytics: boolean;
|
||||
deferred_save_host: boolean;
|
||||
query_reports_disabled: boolean;
|
||||
};
|
||||
smtp_settings: {
|
||||
enable_smtp: boolean;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { IPack } from "./pack";
|
|||
import { ISchedulableQuery } from "./schedulable_query";
|
||||
import { IScheduledQueryStats } from "./scheduled_query_stats";
|
||||
|
||||
export interface IQueryFormData {
|
||||
export interface IEditQueryFormData {
|
||||
description?: string | number | boolean | undefined;
|
||||
name?: string | number | boolean | undefined;
|
||||
query?: string | number | boolean | undefined;
|
||||
|
|
@ -35,7 +35,7 @@ export interface IQuery {
|
|||
stats?: IScheduledQueryStats;
|
||||
}
|
||||
|
||||
export interface IQueryFormFields {
|
||||
export interface IEditQueryFormFields {
|
||||
description: IFormField;
|
||||
name: IFormField;
|
||||
query: IFormField;
|
||||
|
|
|
|||
12
frontend/interfaces/query_report.ts
Normal file
12
frontend/interfaces/query_report.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export interface IQueryReportResultRow {
|
||||
host_id: number;
|
||||
host_name: string;
|
||||
last_fetched: string;
|
||||
columns: any;
|
||||
}
|
||||
|
||||
// Query report
|
||||
export interface IQueryReport {
|
||||
query_id: number;
|
||||
results: IQueryReportResultRow[];
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ export interface ISchedulableQuery {
|
|||
author_name: string;
|
||||
author_email: string;
|
||||
observer_can_run: boolean;
|
||||
discard_data: boolean;
|
||||
packs: IPack[];
|
||||
stats: ISchedulableQueryStats;
|
||||
}
|
||||
|
|
@ -62,6 +63,7 @@ export interface ICreateQueryRequestBody {
|
|||
query: string;
|
||||
description?: string;
|
||||
observer_can_run?: boolean;
|
||||
discard_data?: boolean;
|
||||
team_id?: number; // global query if ommitted
|
||||
interval?: number; // default 0 means never run
|
||||
platform?: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted
|
||||
|
|
@ -81,6 +83,7 @@ export interface IModifyQueryRequestBody
|
|||
query?: string;
|
||||
description?: string;
|
||||
observer_can_run?: boolean;
|
||||
discard_data?: boolean;
|
||||
frequency?: number;
|
||||
platform?: SelectedPlatformString;
|
||||
min_osquery_version?: string;
|
||||
|
|
@ -108,11 +111,12 @@ export interface IDeleteQueriesResponse {
|
|||
deleted: number; // number of queries deleted
|
||||
}
|
||||
|
||||
export interface IQueryFormFields {
|
||||
export interface IEditQueryFormFields {
|
||||
name: IFormField<string>;
|
||||
description: IFormField<string>;
|
||||
query: IFormField<string>;
|
||||
observer_can_run: IFormField<boolean>;
|
||||
discard_data: IFormField<boolean>;
|
||||
frequency: IFormField<number>;
|
||||
platforms: IFormField<SelectedPlatformString>;
|
||||
min_osquery_version: IFormField<string>;
|
||||
|
|
|
|||
|
|
@ -38,14 +38,29 @@ export interface ISelectTeam extends ITeam {
|
|||
|
||||
export type ISelectTargetsEntity = ISelectHost | ISelectLabel | ISelectTeam;
|
||||
|
||||
export interface ISelectedTargets {
|
||||
export interface ISelectedTargetsForApi {
|
||||
hosts: number[];
|
||||
labels: number[];
|
||||
teams: number[];
|
||||
}
|
||||
|
||||
export interface ISelectedTargetsByType {
|
||||
hosts: IHost[];
|
||||
labels: ILabel[];
|
||||
teams: ITeam[];
|
||||
}
|
||||
|
||||
export interface IPackTargets {
|
||||
host_ids: (number | string)[];
|
||||
label_ids: (number | string)[];
|
||||
team_ids: (number | string)[];
|
||||
}
|
||||
|
||||
// TODO: Also use for testing
|
||||
export const DEFAULT_TARGETS: ITarget[] = [];
|
||||
|
||||
export const DEFAULT_TARGETS_BY_TYPE: ISelectedTargetsByType = {
|
||||
hosts: [],
|
||||
labels: [],
|
||||
teams: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const Advanced = ({
|
|||
handleSubmit,
|
||||
isUpdatingSettings,
|
||||
}: IAppConfigFormProps): JSX.Element => {
|
||||
const [formData, setFormData] = useState<any>({
|
||||
const [formData, setFormData] = useState({
|
||||
domain: appConfig.smtp_settings.domain || "",
|
||||
verifySSLCerts: appConfig.smtp_settings.verify_ssl_certs || false,
|
||||
enableStartTLS: appConfig.smtp_settings.enable_start_tls,
|
||||
|
|
@ -26,6 +26,8 @@ const Advanced = ({
|
|||
appConfig.host_expiry_settings.host_expiry_enabled || false,
|
||||
hostExpiryWindow: appConfig.host_expiry_settings.host_expiry_window || 0,
|
||||
disableLiveQuery: appConfig.server_settings.live_query_disabled || false,
|
||||
disableQueryReports:
|
||||
appConfig.server_settings.query_reports_disabled || false,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
@ -35,6 +37,7 @@ const Advanced = ({
|
|||
enableHostExpiry,
|
||||
hostExpiryWindow,
|
||||
disableLiveQuery,
|
||||
disableQueryReports,
|
||||
} = formData;
|
||||
|
||||
const [formErrors, setFormErrors] = useState<IAppConfigFormErrors>({});
|
||||
|
|
@ -69,6 +72,7 @@ const Advanced = ({
|
|||
server_url: appConfig.server_settings.server_url || "",
|
||||
live_query_disabled: disableLiveQuery,
|
||||
enable_analytics: appConfig.server_settings.enable_analytics,
|
||||
query_reports_disabled: disableQueryReports,
|
||||
},
|
||||
smtp_settings: {
|
||||
enable_smtp: appConfig.smtp_settings.enable_smtp || false,
|
||||
|
|
@ -172,6 +176,24 @@ const Advanced = ({
|
|||
>
|
||||
Disable live queries
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
onChange={handleInputChange}
|
||||
name="disableQueryReports"
|
||||
value={disableQueryReports}
|
||||
parseTarget
|
||||
// TODO - update to JSX once tooltip wrapper refactor is merged
|
||||
// TODO - once refactor is merged, have this and bove tooltips disappear more
|
||||
// quickly to get out of users' way
|
||||
tooltip={
|
||||
'<p>Disabling query reports will decrease database usage, <br />\
|
||||
but will prevent you from accessing query results in<br /> \
|
||||
Fleet and will delete existing reports. This can also be<br />\
|
||||
disabled on a per-query basis by enabling "Discard <br />\
|
||||
data". <em>(Default: <b>Off</b>)</em></p>'
|
||||
}
|
||||
>
|
||||
Disable query reports
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import queryAPI from "services/entities/queries";
|
|||
import teamAPI, { ILoadTeamsResponse } from "services/entities/teams";
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import {
|
||||
IHost,
|
||||
|
|
@ -26,6 +27,7 @@ import { ILabel } from "interfaces/label";
|
|||
import { IHostPolicy } from "interfaces/policy";
|
||||
import { IQueryStats } from "interfaces/query_stats";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import {
|
||||
IListQueriesResponse,
|
||||
|
|
@ -39,8 +41,13 @@ import MainContent from "components/MainContent";
|
|||
import InfoBanner from "components/InfoBanner";
|
||||
import BackLink from "components/BackLink";
|
||||
|
||||
import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers";
|
||||
import {
|
||||
normalizeEmptyValues,
|
||||
wrapFleetHelper,
|
||||
TAGGED_TEMPLATES,
|
||||
} from "utilities/helpers";
|
||||
import permissions from "utilities/permissions";
|
||||
import { DEFAULT_QUERY } from "utilities/constants";
|
||||
|
||||
import HostSummaryCard from "../cards/HostSummary";
|
||||
import AboutCard from "../cards/About";
|
||||
|
|
@ -101,12 +108,6 @@ interface IHostDetailsSubNavItem {
|
|||
pathname: string;
|
||||
}
|
||||
|
||||
const TAGGED_TEMPLATES = {
|
||||
queryByHostRoute: (hostId: number | undefined | null) => {
|
||||
return `${hostId ? `?host_ids=${hostId}` : ""}`;
|
||||
},
|
||||
};
|
||||
|
||||
const HostDetailsPage = ({
|
||||
route,
|
||||
router,
|
||||
|
|
@ -137,6 +138,7 @@ const HostDetailsPage = ({
|
|||
setLastEditedQueryCritical,
|
||||
setPolicyTeamId,
|
||||
} = useContext(PolicyContext);
|
||||
const { setSelectedQueryTargetsByType } = useContext(QueryContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
|
|
@ -523,12 +525,15 @@ const HostDetailsPage = ({
|
|||
};
|
||||
|
||||
const onQueryHostCustom = () => {
|
||||
setLastEditedQueryBody(DEFAULT_QUERY.query);
|
||||
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
|
||||
router.push(
|
||||
PATHS.NEW_QUERY() + TAGGED_TEMPLATES.queryByHostRoute(host?.id)
|
||||
);
|
||||
};
|
||||
|
||||
const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => {
|
||||
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
|
||||
router.push(
|
||||
PATHS.EDIT_QUERY(selectedQuery.id) +
|
||||
TAGGED_TEMPLATES.queryByHostRoute(host?.id)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import globalPoliciesAPI from "services/entities/global_policies";
|
|||
import teamPoliciesAPI from "services/entities/team_policies";
|
||||
import hostAPI from "services/entities/hosts";
|
||||
import statusAPI from "services/entities/status";
|
||||
import { QUERIES_PAGE_STEPS } from "utilities/constants";
|
||||
import { LIVE_POLICY_STEPS } from "utilities/constants";
|
||||
|
||||
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
||||
import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor";
|
||||
|
|
@ -127,7 +127,7 @@ const PolicyPage = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]);
|
||||
const [step, setStep] = useState(LIVE_POLICY_STEPS[1]);
|
||||
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
|
||||
const [targetedHosts, setTargetedHosts] = useState<IHost[]>([]);
|
||||
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>([]);
|
||||
|
|
@ -260,7 +260,7 @@ const PolicyPage = ({
|
|||
storedPolicyError,
|
||||
createPolicy,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
|
||||
goToSelectTargets: () => setStep(LIVE_POLICY_STEPS[2]),
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
};
|
||||
|
|
@ -272,8 +272,8 @@ const PolicyPage = ({
|
|||
targetedLabels,
|
||||
targetedTeams,
|
||||
targetsTotalCount,
|
||||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
|
||||
goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
|
||||
goToRunQuery: () => setStep(LIVE_POLICY_STEPS[3]),
|
||||
setSelectedTargets,
|
||||
setTargetedHosts,
|
||||
setTargetedLabels,
|
||||
|
|
@ -285,21 +285,21 @@ const PolicyPage = ({
|
|||
selectedTargets,
|
||||
storedPolicy,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
|
||||
targetsTotalCount,
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
case QUERIES_PAGE_STEPS[2]:
|
||||
case LIVE_POLICY_STEPS[2]:
|
||||
return <SelectTargets {...step2Opts} />;
|
||||
case QUERIES_PAGE_STEPS[3]:
|
||||
case LIVE_POLICY_STEPS[3]:
|
||||
return <RunQuery {...step3Opts} />;
|
||||
default:
|
||||
return <QueryEditor {...step1Opts} />;
|
||||
}
|
||||
};
|
||||
|
||||
const isFirstStep = step === QUERIES_PAGE_STEPS[1];
|
||||
const isFirstStep = step === LIVE_POLICY_STEPS[1];
|
||||
const showSidebar =
|
||||
isFirstStep &&
|
||||
isSidebarOpen &&
|
||||
|
|
|
|||
|
|
@ -34,33 +34,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__observer-query-details {
|
||||
padding: 0 2rem;
|
||||
|
||||
h1 {
|
||||
margin: $pad-large 0;
|
||||
font-size: $large;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
|
||||
.sql-button {
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__query-preview {
|
||||
margin-top: 15px;
|
||||
|
||||
.fleet-ace__label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_content {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
|
@ -177,9 +150,4 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.targets-input {
|
||||
.input-icon-field__icon {
|
||||
top: 34px; // Override styling to include label header
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ const QueryEditor = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
// Function instead of constant eliminates race condition with filteredSoftwarePath
|
||||
// Function instead of constant eliminates race condition with filteredPoliciesPath
|
||||
const backToPoliciesPath = () => {
|
||||
return filteredPoliciesPath || PATHS.MANAGE_POLICIES;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useQuery } from "react-query";
|
|||
import { pick } from "lodash";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { TableContext } from "context/table";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { performanceIndicator } from "utilities/helpers";
|
||||
|
|
@ -20,8 +21,10 @@ import {
|
|||
IQueryKeyQueriesLoadAll,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
|
||||
import queriesAPI from "services/entities/queries";
|
||||
import PATHS from "router/paths";
|
||||
import { DEFAULT_QUERY } from "utilities/constants";
|
||||
import { checkPlatformCompatibility } from "utilities/sql_tools";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
|
@ -87,6 +90,9 @@ const ManageQueriesPage = ({
|
|||
isSandboxMode,
|
||||
config,
|
||||
} = useContext(AppContext);
|
||||
const { setLastEditedQueryBody, setSelectedQueryTargetsByType } = useContext(
|
||||
QueryContext
|
||||
);
|
||||
|
||||
const { setResetSelectedRows } = useContext(TableContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
|
@ -178,7 +184,15 @@ const ManageQueriesPage = ({
|
|||
}
|
||||
}, [location, filteredQueriesPath, setFilteredQueriesPath]);
|
||||
|
||||
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId));
|
||||
// Reset selected targets when returned to this page
|
||||
useEffect(() => {
|
||||
setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE);
|
||||
}, []);
|
||||
|
||||
const onCreateQueryClick = () => {
|
||||
setLastEditedQueryBody(DEFAULT_QUERY.query);
|
||||
router.push(PATHS.NEW_QUERY(currentTeamId));
|
||||
};
|
||||
|
||||
const toggleDeleteQueryModal = useCallback(() => {
|
||||
setShowDeleteQueryModal(!showDeleteQueryModal);
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ const generateTableHeaders = ({
|
|||
)}
|
||||
</>
|
||||
}
|
||||
path={PATHS.EDIT_QUERY(
|
||||
path={PATHS.QUERY(
|
||||
cellProps.row.original.id,
|
||||
cellProps.row.original.team_id ?? undefined
|
||||
)}
|
||||
|
|
@ -183,10 +183,7 @@ const generateTableHeaders = ({
|
|||
<TextCell
|
||||
value={val}
|
||||
emptyCellTooltipText={
|
||||
<>
|
||||
Assign a frequency and turn <strong>automations</strong> on to
|
||||
collect data at an interval.
|
||||
</>
|
||||
<>Assign a frequency to collect data at an interval.</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,293 +0,0 @@
|
|||
import React, { useState, useEffect, useContext, useCallback } from "react";
|
||||
import { useQuery, useMutation } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { QUERIES_PAGE_STEPS, DEFAULT_QUERY } from "utilities/constants";
|
||||
import queryAPI from "services/entities/queries";
|
||||
import hostAPI from "services/entities/hosts";
|
||||
import statusAPI from "services/entities/status";
|
||||
import { IHost, IHostResponse } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import {
|
||||
IGetQueryResponse,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { ITarget } from "interfaces/target";
|
||||
|
||||
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
||||
import MainContent from "components/MainContent";
|
||||
import SidePanelContent from "components/SidePanelContent";
|
||||
import SelectTargets from "components/LiveQuery/SelectTargets";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import QueryEditor from "pages/queries/QueryPage/screens/QueryEditor";
|
||||
import RunQuery from "pages/queries/QueryPage/screens/RunQuery";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
|
||||
interface IQueryPageProps {
|
||||
router: InjectedRouter;
|
||||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
query: { host_ids: string; team_id?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
const baseClass = "query-page";
|
||||
|
||||
const QueryPage = ({
|
||||
router,
|
||||
params: { id: paramsQueryId },
|
||||
location,
|
||||
}: IQueryPageProps): JSX.Element => {
|
||||
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
||||
const {
|
||||
currentTeamName: teamNameForQuery,
|
||||
teamIdForApi: apiTeamIdForQuery,
|
||||
} = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
isObserverPlus,
|
||||
isAnyTeamObserverPlus,
|
||||
} = useContext(AppContext);
|
||||
const {
|
||||
selectedOsqueryTable,
|
||||
setSelectedOsqueryTable,
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryPlatforms,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
|
||||
const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]);
|
||||
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
|
||||
const [targetedHosts, setTargetedHosts] = useState<IHost[]>([]);
|
||||
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>([]);
|
||||
const [targetedTeams, setTargetedTeams] = useState<ITeam[]>([]);
|
||||
const [targetsTotalCount, setTargetsTotalCount] = useState(0);
|
||||
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
|
||||
false
|
||||
);
|
||||
|
||||
// disabled on page load so we can control the number of renders
|
||||
// else it will re-populate the context on occasion
|
||||
const {
|
||||
isLoading: isStoredQueryLoading,
|
||||
data: storedQuery,
|
||||
error: storedQueryError,
|
||||
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
||||
["query", queryId],
|
||||
() => queryAPI.load(queryId as number),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => data.query,
|
||||
onSuccess: (returnedQuery) => {
|
||||
setLastEditedQueryId(returnedQuery.id);
|
||||
setLastEditedQueryName(returnedQuery.name);
|
||||
setLastEditedQueryDescription(returnedQuery.description);
|
||||
setLastEditedQueryBody(returnedQuery.query);
|
||||
setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
|
||||
setLastEditedQueryFrequency(returnedQuery.interval);
|
||||
setLastEditedQueryPlatforms(returnedQuery.platform);
|
||||
setLastEditedQueryLoggingType(returnedQuery.logging);
|
||||
setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
|
||||
},
|
||||
onError: (error) => handlePageError(error),
|
||||
}
|
||||
);
|
||||
|
||||
useQuery<IHostResponse, Error, IHost>(
|
||||
"hostFromURL",
|
||||
() =>
|
||||
hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
|
||||
{
|
||||
enabled: !!location.query.host_ids && !queryParamHostsAdded,
|
||||
select: (data: IHostResponse) => data.host,
|
||||
onSuccess: (host) => {
|
||||
setTargetedHosts((prevHosts) =>
|
||||
prevHosts.filter((h) => h.id !== host.id).concat(host)
|
||||
);
|
||||
const targets = selectedTargets;
|
||||
host.target_type = "hosts";
|
||||
targets.push(host);
|
||||
setSelectedTargets([...targets]);
|
||||
if (!queryParamHostsAdded) {
|
||||
setQueryParamHostsAdded(true);
|
||||
}
|
||||
router.replace(location.pathname);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const detectIsFleetQueryRunnable = () => {
|
||||
statusAPI.live_query().catch(() => {
|
||||
setIsLiveQueryRunnable(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
detectIsFleetQueryRunnable();
|
||||
if (!queryId) {
|
||||
setLastEditedQueryId(DEFAULT_QUERY.id);
|
||||
setLastEditedQueryName(DEFAULT_QUERY.name);
|
||||
setLastEditedQueryDescription(DEFAULT_QUERY.description);
|
||||
setLastEditedQueryBody(DEFAULT_QUERY.query);
|
||||
setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
|
||||
setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
|
||||
setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
|
||||
setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version);
|
||||
setLastEditedQueryPlatforms(DEFAULT_QUERY.platform);
|
||||
}
|
||||
}, [queryId]);
|
||||
|
||||
// Updates title that shows up on browser tabs
|
||||
useEffect(() => {
|
||||
// e.g., Query details | Discover TLS certificates | Fleet for osquery
|
||||
document.title = `Query details | ${storedQuery?.name} | Fleet for osquery`;
|
||||
}, [location.pathname, storedQuery?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowOpenSchemaActionText(!isSidebarOpen);
|
||||
}, [isSidebarOpen]);
|
||||
|
||||
const onOsqueryTableSelect = (tableName: string) => {
|
||||
setSelectedOsqueryTable(tableName);
|
||||
};
|
||||
|
||||
const onCloseSchemaSidebar = () => {
|
||||
setIsSidebarOpen(false);
|
||||
};
|
||||
|
||||
const onOpenSchemaSidebar = () => {
|
||||
setIsSidebarOpen(true);
|
||||
};
|
||||
|
||||
const renderLiveQueryWarning = (): JSX.Element | null => {
|
||||
if (isLiveQueryRunnable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__warning`}>
|
||||
<div className={`${baseClass}__message`}>
|
||||
<p>
|
||||
Fleet is unable to run a live query. Refresh the page or log in
|
||||
again. If this keeps happening please{" "}
|
||||
<CustomLink
|
||||
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
||||
text="file an issue"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []);
|
||||
|
||||
const renderScreen = () => {
|
||||
const step1Props = {
|
||||
router,
|
||||
baseClass,
|
||||
queryIdForEdit: queryId,
|
||||
teamNameForQuery,
|
||||
apiTeamIdForQuery,
|
||||
showOpenSchemaActionText,
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
storedQueryError,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
};
|
||||
|
||||
const step2Props = {
|
||||
baseClass,
|
||||
queryId,
|
||||
selectedTargets,
|
||||
targetedHosts,
|
||||
targetedLabels,
|
||||
targetedTeams,
|
||||
targetsTotalCount,
|
||||
goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
|
||||
goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
|
||||
setSelectedTargets,
|
||||
setTargetedHosts,
|
||||
setTargetedLabels,
|
||||
setTargetedTeams,
|
||||
setTargetsTotalCount,
|
||||
};
|
||||
|
||||
const step3Props = {
|
||||
queryId,
|
||||
selectedTargets,
|
||||
storedQuery,
|
||||
setSelectedTargets,
|
||||
goToQueryEditor,
|
||||
targetsTotalCount,
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
case QUERIES_PAGE_STEPS[2]:
|
||||
return <SelectTargets {...step2Props} />;
|
||||
case QUERIES_PAGE_STEPS[3]:
|
||||
return <RunQuery {...step3Props} />;
|
||||
default:
|
||||
return <QueryEditor {...step1Props} />;
|
||||
}
|
||||
};
|
||||
|
||||
const isFirstStep = step === QUERIES_PAGE_STEPS[1];
|
||||
const showSidebar =
|
||||
isFirstStep &&
|
||||
isSidebarOpen &&
|
||||
(isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isAnyTeamMaintainerOrTeamAdmin ||
|
||||
isObserverPlus ||
|
||||
isAnyTeamObserverPlus);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}_wrapper`}>{renderScreen()}</div>
|
||||
</MainContent>
|
||||
{showSidebar && (
|
||||
<SidePanelContent>
|
||||
<QuerySidePanel
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
selectedOsqueryTable={selectedOsqueryTable}
|
||||
onClose={onCloseSchemaSidebar}
|
||||
/>
|
||||
</SidePanelContent>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryPage;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./QueryForm";
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
.save-query-modal {
|
||||
.fleet-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: $pad-small;
|
||||
margin-bottom: $pad-large;
|
||||
font-weight: $regular;
|
||||
font-size: 0.75rem;
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
|
||||
&__form-field {
|
||||
&--frequency {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&--platform {
|
||||
margin-bottom: 0;
|
||||
margin-top: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__observer-can-run-wrapper {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__advanced-options-toggle {
|
||||
font-weight: $xbold;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./QueryPage";
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
import { UseMutateAsyncFunction } from "react-query";
|
||||
|
||||
import queryAPI from "services/entities/queries";
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import PATHS from "router/paths";
|
||||
import debounce from "utilities/debounce";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
|
||||
import BackLink from "components/BackLink";
|
||||
import QueryForm from "pages/queries/QueryPage/components/QueryForm";
|
||||
|
||||
interface IQueryEditorProps {
|
||||
router: InjectedRouter;
|
||||
baseClass: string;
|
||||
queryIdForEdit: number | null;
|
||||
teamNameForQuery?: string;
|
||||
apiTeamIdForQuery?: number;
|
||||
storedQuery: ISchedulableQuery | undefined;
|
||||
storedQueryError: Error | null;
|
||||
showOpenSchemaActionText: boolean;
|
||||
isStoredQueryLoading: boolean;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
onOpenSchemaSidebar: () => void;
|
||||
renderLiveQueryWarning: () => JSX.Element | null;
|
||||
}
|
||||
|
||||
const QueryEditor = ({
|
||||
router,
|
||||
baseClass,
|
||||
queryIdForEdit,
|
||||
teamNameForQuery,
|
||||
apiTeamIdForQuery,
|
||||
storedQuery,
|
||||
storedQueryError,
|
||||
showOpenSchemaActionText,
|
||||
isStoredQueryLoading,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
}: IQueryEditorProps): JSX.Element | null => {
|
||||
const { currentUser, filteredQueriesPath } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
// Note: The QueryContext values should always be used for any mutable query data such as query name
|
||||
// The storedQuery prop should only be used to access immutable metadata such as author id
|
||||
const {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [isQuerySaving, setIsQuerySaving] = useState(false);
|
||||
const [isQueryUpdating, setIsQueryUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (storedQueryError) {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Something went wrong retrieving your query. Please try again."
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
|
||||
const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
|
||||
setIsQuerySaving(true);
|
||||
try {
|
||||
const { query } = await queryAPI.create(formData);
|
||||
router.push(PATHS.EDIT_QUERY(query.id));
|
||||
renderFlash("success", "Query created!");
|
||||
setBackendValidators({});
|
||||
} catch (createError: any) {
|
||||
if (createError.data.errors[0].reason.includes("already exists")) {
|
||||
const teamErrorText =
|
||||
teamNameForQuery && apiTeamIdForQuery !== 0
|
||||
? `the ${teamNameForQuery} team`
|
||||
: "all teams";
|
||||
setBackendValidators({
|
||||
name: `A query with that name already exists for ${teamErrorText}.`,
|
||||
});
|
||||
} else {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Something went wrong creating your query. Please try again."
|
||||
);
|
||||
setBackendValidators({});
|
||||
}
|
||||
} finally {
|
||||
setIsQuerySaving(false);
|
||||
}
|
||||
});
|
||||
|
||||
const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
|
||||
if (!queryIdForEdit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsQueryUpdating(true);
|
||||
|
||||
const updatedQuery = deepDifference(formData, {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
});
|
||||
|
||||
try {
|
||||
await queryAPI.update(queryIdForEdit, updatedQuery);
|
||||
renderFlash("success", "Query updated!");
|
||||
} catch (updateError: any) {
|
||||
console.error(updateError);
|
||||
if (updateError.data.errors[0].reason.includes("Duplicate")) {
|
||||
renderFlash("error", "A query with this name already exists.");
|
||||
} else {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Something went wrong updating your query. Please try again."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setIsQueryUpdating(false);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function instead of constant eliminates race condition with filteredSoftwarePath
|
||||
const backToQueriesPath = () => {
|
||||
return filteredQueriesPath || PATHS.MANAGE_QUERIES;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__form`}>
|
||||
<div className={`${baseClass}__header-links`}>
|
||||
<BackLink text="Back to queries" path={backToQueriesPath()} />
|
||||
</div>
|
||||
<QueryForm
|
||||
router={router}
|
||||
saveQuery={saveQuery}
|
||||
goToSelectTargets={goToSelectTargets}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onUpdate={onUpdateQuery}
|
||||
storedQuery={storedQuery}
|
||||
queryIdForEdit={queryIdForEdit}
|
||||
apiTeamIdForQuery={apiTeamIdForQuery}
|
||||
teamNameForQuery={teamNameForQuery}
|
||||
isStoredQueryLoading={isStoredQueryLoading}
|
||||
showOpenSchemaActionText={showOpenSchemaActionText}
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
renderLiveQueryWarning={renderLiveQueryWarning}
|
||||
backendValidators={backendValidators}
|
||||
isQuerySaving={isQuerySaving}
|
||||
isQueryUpdating={isQueryUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryEditor;
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
import React, { useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
|
||||
import {
|
||||
IGetQueryResponse,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { IQueryReport } from "interfaces/query_report";
|
||||
|
||||
import queryAPI from "services/entities/queries";
|
||||
import queryReportAPI, { ISortOption } from "services/entities/query_report";
|
||||
|
||||
import Spinner from "components/Spinner/Spinner";
|
||||
import Button from "components/buttons/Button";
|
||||
import BackLink from "components/BackLink";
|
||||
import MainContent from "components/MainContent";
|
||||
import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
|
||||
import QueryAutomationsStatusIndicator from "pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator";
|
||||
import DataError from "components/DataError/DataError";
|
||||
import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import InfoBanner from "components/InfoBanner";
|
||||
import QueryReport from "../components/QueryReport/QueryReport";
|
||||
import NoResults from "../components/NoResults/NoResults";
|
||||
|
||||
import {
|
||||
DEFAULT_SORT_HEADER,
|
||||
DEFAULT_SORT_DIRECTION,
|
||||
QUERY_REPORT_RESULTS_LIMIT,
|
||||
} from "./QueryDetailsPageConfig";
|
||||
|
||||
interface IQueryDetailsPageProps {
|
||||
router: InjectedRouter; // v3
|
||||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
query: { team_id?: string; order_key?: string; order_direction?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
const baseClass = "query-details-page";
|
||||
|
||||
const QueryDetailsPage = ({
|
||||
router,
|
||||
params: { id: paramsQueryId },
|
||||
location,
|
||||
}: IQueryDetailsPageProps): JSX.Element => {
|
||||
const queryId = parseInt(paramsQueryId, 10);
|
||||
const queryParams = location.query;
|
||||
|
||||
// Functions to avoid race conditions
|
||||
const serverSortBy: ISortOption[] = (() => {
|
||||
return [
|
||||
{
|
||||
key: queryParams?.order_key ?? DEFAULT_SORT_HEADER,
|
||||
direction: queryParams?.order_direction ?? DEFAULT_SORT_DIRECTION,
|
||||
},
|
||||
];
|
||||
})();
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
isObserverPlus,
|
||||
isAnyTeamObserverPlus,
|
||||
config,
|
||||
filteredQueriesPath,
|
||||
} = useContext(AppContext);
|
||||
const {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryPlatforms,
|
||||
setLastEditedQueryDiscardData,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
// Title that shows up on browser tabs (e.g., Query details | Discover TLS certificates | Fleet for osquery)
|
||||
document.title = `Query details | ${lastEditedQueryName} | Fleet for osquery`;
|
||||
|
||||
// disabled on page load so we can control the number of renders
|
||||
// else it will re-populate the context on occasion
|
||||
const {
|
||||
isLoading: isStoredQueryLoading,
|
||||
data: storedQuery,
|
||||
error: storedQueryError,
|
||||
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
||||
["query", queryId],
|
||||
() => queryAPI.load(queryId),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => data.query,
|
||||
onSuccess: (returnedQuery) => {
|
||||
setLastEditedQueryId(returnedQuery.id);
|
||||
setLastEditedQueryName(returnedQuery.name);
|
||||
setLastEditedQueryDescription(returnedQuery.description);
|
||||
setLastEditedQueryBody(returnedQuery.query);
|
||||
setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
|
||||
setLastEditedQueryFrequency(returnedQuery.interval);
|
||||
setLastEditedQueryPlatforms(returnedQuery.platform);
|
||||
setLastEditedQueryLoggingType(returnedQuery.logging);
|
||||
setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
|
||||
setLastEditedQueryDiscardData(returnedQuery.discard_data);
|
||||
},
|
||||
onError: (error) => handlePageError(error),
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading: isQueryReportLoading,
|
||||
data: queryReport,
|
||||
error: queryReportError,
|
||||
} = useQuery<IQueryReport, Error, IQueryReport>(
|
||||
[],
|
||||
() =>
|
||||
queryReportAPI.load({
|
||||
sortBy: serverSortBy,
|
||||
id: queryId,
|
||||
}),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (error) => handlePageError(error),
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = isStoredQueryLoading || isQueryReportLoading;
|
||||
const isApiError = storedQueryError || queryReportError;
|
||||
const isClipped =
|
||||
(queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT;
|
||||
|
||||
const renderHeader = () => {
|
||||
const canEditQuery =
|
||||
isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainerOrTeamAdmin;
|
||||
|
||||
// Function instead of constant eliminates race condition with filteredQueriesPath
|
||||
const backToQueriesPath = () => {
|
||||
return filteredQueriesPath || PATHS.MANAGE_QUERIES;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__header-links`}>
|
||||
<BackLink text="Back to queries" path={backToQueriesPath()} />
|
||||
</div>
|
||||
<div className={`${baseClass}__header-details`}>
|
||||
{!isLoading && !isApiError && (
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className="name-description">
|
||||
<h1 className={`${baseClass}__query-name`}>
|
||||
{lastEditedQueryName}
|
||||
</h1>
|
||||
<p className={`${baseClass}__query-description`}>
|
||||
{lastEditedQueryDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{canEditQuery && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
queryId && router.push(PATHS.EDIT_QUERY(queryId));
|
||||
}}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
variant="brand"
|
||||
>
|
||||
Edit query
|
||||
</Button>
|
||||
)}
|
||||
{(lastEditedQueryObserverCanRun ||
|
||||
isObserverPlus ||
|
||||
isAnyTeamObserverPlus ||
|
||||
canEditQuery) && (
|
||||
<div
|
||||
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--new-query`}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__run`}
|
||||
variant="blue-green"
|
||||
onClick={() => {
|
||||
queryId && router.push(PATHS.LIVE_QUERY(queryId));
|
||||
}}
|
||||
>
|
||||
Live query
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !isApiError && (
|
||||
<div className={`${baseClass}__settings`}>
|
||||
<div className={`${baseClass}__automations`}>
|
||||
<TooltipWrapper
|
||||
tipContent={`Query automations let you send data to your log destination on a schedule. When automations are <b>on</b>, data is sent according to a query’s frequency.`}
|
||||
>
|
||||
Automations:
|
||||
</TooltipWrapper>
|
||||
<QueryAutomationsStatusIndicator
|
||||
automationsEnabled={storedQuery?.automations_enabled || false}
|
||||
interval={storedQuery?.interval || 0}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__log-destination`}>
|
||||
<strong>Log destination:</strong>{" "}
|
||||
<LogDestinationIndicator
|
||||
logDestination={config?.logging.result.plugin || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderClippedBanner = () => (
|
||||
<InfoBanner
|
||||
color="yellow"
|
||||
cta={
|
||||
<CustomLink
|
||||
url="https://www.fleetdm.com/support"
|
||||
text="Get help"
|
||||
newTab
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<b>Report clipped.</b> A sample of this query's results is included
|
||||
below. You can still use query automations to complete this report in
|
||||
your log destination.
|
||||
</div>
|
||||
</InfoBanner>
|
||||
);
|
||||
|
||||
const renderReport = () => {
|
||||
const disabledCachingGlobally =
|
||||
config?.server_settings.query_reports_disabled || true;
|
||||
const discardDataEnabled = storedQuery?.discard_data || true;
|
||||
const loggingSnapshot = storedQuery?.logging === "snapshot";
|
||||
const disabledCaching =
|
||||
disabledCachingGlobally || discardDataEnabled || !loggingSnapshot;
|
||||
const emptyCache = (queryReport?.results?.length ?? 0) === 0;
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (isApiError) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
||||
// Empty state with varying messages explaining why there's no results
|
||||
if (emptyCache) {
|
||||
return (
|
||||
<NoResults
|
||||
queryInterval={storedQuery?.interval}
|
||||
queryUpdatedAt={storedQuery?.updated_at}
|
||||
disabledCaching={disabledCaching}
|
||||
disabledCachingGlobally={disabledCachingGlobally}
|
||||
discardDataEnabled={discardDataEnabled}
|
||||
loggingSnapshot={loggingSnapshot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <QueryReport queryReport={queryReport} isClipped={isClipped} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
{renderHeader()}
|
||||
{isClipped && renderClippedBanner()}
|
||||
{renderReport()}
|
||||
</div>
|
||||
</MainContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryDetailsPage;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// TODO
|
||||
export const QUERY_DETAILS_PAGE_FILTER_KEYS = ["model", "vendor"] as const;
|
||||
|
||||
// TODO: refactor to use this type as the location.query prop of the page
|
||||
export type QueryDetailsPageQueryParams = Record<
|
||||
| "order_key"
|
||||
| "order_direction"
|
||||
| typeof QUERY_DETAILS_PAGE_FILTER_KEYS[number],
|
||||
string
|
||||
>;
|
||||
|
||||
export const DEFAULT_SORT_HEADER = "host_name";
|
||||
export const DEFAULT_SORT_DIRECTION = "asc";
|
||||
|
||||
export const QUERY_REPORT_RESULTS_LIMIT = 1000;
|
||||
79
frontend/pages/queries/details/QueryDetailsPage/_styles.scss
Normal file
79
frontend/pages/queries/details/QueryDetailsPage/_styles.scss
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
.query-details-page {
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-large;
|
||||
}
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: $pad-small;
|
||||
|
||||
.name-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 266px;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&__query-name {
|
||||
margin-top: 0;
|
||||
font-size: $large;
|
||||
}
|
||||
|
||||
&__query-description {
|
||||
margin-top: 0;
|
||||
margin-bottom: $pad-small;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__settings {
|
||||
display: flex;
|
||||
gap: $pad-large;
|
||||
font-size: $x-small;
|
||||
// TODO - remove once refactored tooltip wrapper is merged
|
||||
.component__tooltip-wrapper__element__underline::after {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__automations,
|
||||
&__log-destination {
|
||||
display: flex;
|
||||
gap: $pad-small;
|
||||
align-items: center;
|
||||
|
||||
.component__tooltip-wrapper__element {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-table__inner {
|
||||
.component__tooltip-wrapper__tip-text {
|
||||
text-align: left;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
ul {
|
||||
color: $core-white;
|
||||
|
||||
li {
|
||||
&::before {
|
||||
content: "•";
|
||||
color: $core-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-error {
|
||||
padding-top: $pad-xxxlarge;
|
||||
}
|
||||
}
|
||||
1
frontend/pages/queries/details/QueryDetailsPage/index.ts
Normal file
1
frontend/pages/queries/details/QueryDetailsPage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./QueryDetailsPage";
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import React from "react";
|
||||
|
||||
import differenceInSeconds from "date-fns/differenceInSeconds";
|
||||
import formatDistance from "date-fns/formatDistance";
|
||||
import add from "date-fns/add";
|
||||
|
||||
import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
|
||||
import EmptyTable from "components/EmptyTable/EmptyTable";
|
||||
|
||||
interface INoResultsProps {
|
||||
queryInterval?: number;
|
||||
queryUpdatedAt?: string;
|
||||
disabledCaching: boolean;
|
||||
disabledCachingGlobally: boolean;
|
||||
discardDataEnabled: boolean;
|
||||
loggingSnapshot: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "no-results";
|
||||
|
||||
const NoResults = ({
|
||||
queryInterval,
|
||||
queryUpdatedAt,
|
||||
disabledCaching,
|
||||
disabledCachingGlobally,
|
||||
discardDataEnabled,
|
||||
loggingSnapshot,
|
||||
}: INoResultsProps): JSX.Element => {
|
||||
// Returns how many seconds it takes to expect a cached update
|
||||
const secondsCheckbackTime = () => {
|
||||
const secondsSinceUpdate = queryUpdatedAt
|
||||
? differenceInSeconds(new Date(), new Date(queryUpdatedAt))
|
||||
: 0;
|
||||
const secondsUpdateWaittime = (queryInterval || 0) + 60;
|
||||
return secondsUpdateWaittime - secondsSinceUpdate;
|
||||
};
|
||||
|
||||
// Update status of collecting cached results
|
||||
const collectingResults = secondsCheckbackTime() > 0;
|
||||
|
||||
// Converts seconds takes to update to human readable format
|
||||
const readableCheckbackTime = formatDistance(
|
||||
add(new Date(), { seconds: secondsCheckbackTime() }),
|
||||
new Date()
|
||||
);
|
||||
|
||||
// Collecting results state
|
||||
if (collectingResults) {
|
||||
const collectingResultsInfo = () =>
|
||||
`Fleet is collecting query results. Check back in about ${readableCheckbackTime}.`;
|
||||
|
||||
return (
|
||||
<EmptyTable
|
||||
iconName="collecting-results"
|
||||
header={"Collecting results..."}
|
||||
info={collectingResultsInfo()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const noResultsInfo = () => {
|
||||
if (!queryInterval) {
|
||||
return (
|
||||
<>
|
||||
This query does not collect data on a schedule. Add a{" "}
|
||||
<strong>frequency</strong> or run this as a live query to see results.
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (disabledCaching) {
|
||||
const tipContent = () => {
|
||||
if (disabledCachingGlobally) {
|
||||
return "The following setting prevents saving this query's results in Fleet:<ul><li>Query reports are globally disabled in organization settings.</li></ul>";
|
||||
}
|
||||
if (discardDataEnabled) {
|
||||
return "The following setting prevents saving this query's results in Fleet:<ul><li>This query has Discard data enabled.</li></ul>";
|
||||
}
|
||||
if (!loggingSnapshot) {
|
||||
return "The following setting prevents saving this query's results in Fleet:<ul><li>The logging setting for this query is not Snapshot.</li></ul>";
|
||||
}
|
||||
return "Unknown";
|
||||
};
|
||||
return (
|
||||
<>
|
||||
Results from this query are{" "}
|
||||
<TooltipWrapper tipContent={tipContent()}>
|
||||
not reported in Fleet
|
||||
</TooltipWrapper>
|
||||
.
|
||||
</>
|
||||
);
|
||||
}
|
||||
// No errors will be reported in V1
|
||||
// if (errorsOnly) {
|
||||
// return (
|
||||
// <>
|
||||
// This query had trouble collecting data on some hosts. Check out the{" "}
|
||||
// <strong>Errors</strong> tab to see why.
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
return "This query has returned no data so far.";
|
||||
};
|
||||
|
||||
return (
|
||||
<EmptyTable
|
||||
className={baseClass}
|
||||
iconName="empty-software"
|
||||
header={"Nothing to report yet"}
|
||||
info={noResultsInfo()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoResults;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./NoResults";
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import React, { useState, useContext, useEffect, useCallback } from "react";
|
||||
|
||||
import { Row, Column } from "react-table";
|
||||
import FileSaver from "file-saver";
|
||||
import { QueryContext } from "context/query";
|
||||
|
||||
import {
|
||||
generateCSVFilename,
|
||||
generateCSVQueryResults,
|
||||
} from "utilities/generate_csv";
|
||||
import { IQueryReport, IQueryReportResultRow } from "interfaces/query_report";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import ShowQueryModal from "components/modals/ShowQueryModal";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
import generateResultsTableHeaders from "./QueryReportTableConfig";
|
||||
|
||||
interface IQueryReportProps {
|
||||
queryReport?: IQueryReport;
|
||||
isClipped?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "query-report";
|
||||
const CSV_TITLE = "Query";
|
||||
|
||||
const tableResults = (results: IQueryReportResultRow[]) => {
|
||||
return results.map((result: IQueryReportResultRow) => {
|
||||
const hostInfoColumns = {
|
||||
host_display_name: result.host_name,
|
||||
last_fetched: result.last_fetched,
|
||||
};
|
||||
|
||||
// hostInfoColumns displays the host metadata that is returned with every query
|
||||
// result.columns are the variable columns returned by the API that differ per query
|
||||
return { ...hostInfoColumns, ...result.columns };
|
||||
});
|
||||
};
|
||||
|
||||
const QueryReport = ({
|
||||
queryReport,
|
||||
isClipped,
|
||||
}: IQueryReportProps): JSX.Element => {
|
||||
const { lastEditedQueryName, lastEditedQueryBody } = useContext(QueryContext);
|
||||
|
||||
const [showQueryModal, setShowQueryModal] = useState(false);
|
||||
const [filteredResults, setFilteredResults] = useState<Row[]>(
|
||||
tableResults(queryReport?.results || [])
|
||||
);
|
||||
const [tableHeaders, setTableHeaders] = useState<Column[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryReport && queryReport.results && queryReport.results.length > 0) {
|
||||
const generatedTableHeaders = generateResultsTableHeaders(
|
||||
tableResults(queryReport.results)
|
||||
);
|
||||
// Update tableHeaders if new headers are found
|
||||
if (generatedTableHeaders !== tableHeaders) {
|
||||
setTableHeaders(generatedTableHeaders);
|
||||
}
|
||||
}
|
||||
}, [queryReport]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders
|
||||
|
||||
const onExportQueryResults = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
evt.preventDefault();
|
||||
FileSaver.saveAs(
|
||||
generateCSVQueryResults(
|
||||
filteredResults,
|
||||
generateCSVFilename(
|
||||
`${lastEditedQueryName || CSV_TITLE} - Query Report`
|
||||
),
|
||||
tableHeaders
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const onShowQueryModal = () => {
|
||||
setShowQueryModal(!showQueryModal);
|
||||
};
|
||||
|
||||
const renderNoResults = () => {
|
||||
return <p className="no-results-message">TODO</p>;
|
||||
};
|
||||
|
||||
const renderTableButtons = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__results-cta`}>
|
||||
<Button
|
||||
className={`${baseClass}__show-query-btn`}
|
||||
onClick={onShowQueryModal}
|
||||
variant="text-icon"
|
||||
>
|
||||
<>
|
||||
Show query <Icon name="eye" />
|
||||
</>
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportQueryResults}
|
||||
variant="text-icon"
|
||||
>
|
||||
<>
|
||||
Export results
|
||||
<Icon name="download" color="core-fleet-blue" />
|
||||
</>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderResultsCount = useCallback(() => {
|
||||
const count = filteredResults.length;
|
||||
|
||||
if (isClipped) {
|
||||
return (
|
||||
<div className={`${baseClass}__count `}>
|
||||
<TooltipWrapper
|
||||
tipContent={`Fleet has retained a sample of early results for
|
||||
reference. Reporting is paused until existing data is deleted. <br/><br/>
|
||||
You can reset this report by updating the query's SQL, or by
|
||||
temporarily enabling the <b>discard data</b> setting and disabling it again.`}
|
||||
>
|
||||
{`${count} result${count === 1 ? "" : "s"}`}
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={`${baseClass}__count `}>
|
||||
<span>{`${count} result${count === 1 ? "" : "s"}`}</span>
|
||||
</div>
|
||||
);
|
||||
}, [filteredResults.length, isClipped]);
|
||||
|
||||
const renderTable = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__results-table-container`}>
|
||||
<TableContainer
|
||||
columns={tableHeaders}
|
||||
data={tableResults(queryReport?.results || [])}
|
||||
emptyComponent={renderNoResults}
|
||||
isLoading={false}
|
||||
isClientSidePagination
|
||||
isClientSideFilter
|
||||
isMultiColumnFilter
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
resultsTitle="results"
|
||||
customControl={() => renderTableButtons()}
|
||||
setExportRows={setFilteredResults}
|
||||
renderCount={renderResultsCount}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
{renderTable()}
|
||||
{showQueryModal && (
|
||||
<ShowQueryModal
|
||||
query={lastEditedQueryBody}
|
||||
onCancel={onShowQueryModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryReport;
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
// disable this rule as it was throwing an error in Header and Cell component
|
||||
// definitions for the selection row for some reason when we dont really need it.
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
CellProps,
|
||||
Column,
|
||||
ColumnInstance,
|
||||
ColumnInterface,
|
||||
HeaderProps,
|
||||
TableInstance,
|
||||
} from "react-table";
|
||||
|
||||
import DefaultColumnFilter from "components/TableContainer/DataTable/DefaultColumnFilter";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
|
||||
import { humanHostLastSeen } from "utilities/helpers";
|
||||
|
||||
type IHeaderProps = HeaderProps<TableInstance> & {
|
||||
column: ColumnInstance & IDataColumn;
|
||||
};
|
||||
|
||||
type ICellProps = CellProps<TableInstance>;
|
||||
|
||||
interface IDataColumn extends ColumnInterface {
|
||||
title?: string;
|
||||
accessor: string;
|
||||
}
|
||||
|
||||
const _unshiftHostname = (headers: IDataColumn[]) => {
|
||||
const newHeaders = [...headers];
|
||||
const displayNameIndex = headers.findIndex(
|
||||
(h) => h.id === "host_display_name"
|
||||
);
|
||||
if (displayNameIndex >= 0) {
|
||||
// remove hostname header from headers
|
||||
const [displayNameHeader] = newHeaders.splice(displayNameIndex, 1);
|
||||
// reformat title and insert at start of headers array
|
||||
newHeaders.unshift({ ...displayNameHeader, title: "Host" });
|
||||
}
|
||||
// TODO: Remove after v5 when host_hostname is removed rom API response.
|
||||
const hostNameIndex = headers.findIndex((h) => h.id === "host_hostname");
|
||||
if (hostNameIndex >= 0) {
|
||||
newHeaders.splice(hostNameIndex, 1);
|
||||
}
|
||||
// end remove
|
||||
return newHeaders;
|
||||
};
|
||||
|
||||
const generateResultsTableHeaders = (results: any[]): Column[] => {
|
||||
/* Results include an array of objects, each representing a table row
|
||||
Each key value pair in an object represents a column name and value
|
||||
To create headers, use JS set to create an array of all unique column names */
|
||||
const uniqueColumnNames = Array.from(
|
||||
results.reduce(
|
||||
(s, o) => Object.keys(o).reduce((t, k) => t.add(k), s),
|
||||
new Set() // Set prevents listing duplicate headers
|
||||
)
|
||||
);
|
||||
|
||||
const headers = uniqueColumnNames.map((key) => {
|
||||
return {
|
||||
id: key as string,
|
||||
title: key as string,
|
||||
Header: (headerProps: IHeaderProps) => (
|
||||
<HeaderCell
|
||||
value={
|
||||
// Sentence case last fetched
|
||||
headerProps.column.title === "last_fetched"
|
||||
? "Last fetched"
|
||||
: headerProps.column.title || headerProps.column.id
|
||||
}
|
||||
isSortedDesc={headerProps.column.isSortedDesc}
|
||||
/>
|
||||
),
|
||||
accessor: key as string,
|
||||
Cell: (cellProps: ICellProps) => {
|
||||
// Sorts chronologically by date, but UI displays readable last fetched
|
||||
if (cellProps.column.id === "last_fetched") {
|
||||
return humanHostLastSeen(cellProps?.cell?.value);
|
||||
}
|
||||
return cellProps?.cell?.value || null;
|
||||
},
|
||||
Filter: DefaultColumnFilter, // Component hides filter for last_fetched
|
||||
filterType: "text",
|
||||
disableSortBy: false,
|
||||
};
|
||||
});
|
||||
return _unshiftHostname(headers);
|
||||
};
|
||||
|
||||
export default generateResultsTableHeaders;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
.query-report {
|
||||
&__wrapper {
|
||||
.host_id__header {
|
||||
width: 95px; // Min width for 6 digits host IDs
|
||||
}
|
||||
|
||||
.last_fetched__header {
|
||||
.column-header {
|
||||
margin-bottom: 44px; // Fills space where filter is removed
|
||||
}
|
||||
}
|
||||
|
||||
.data-table__wrapper {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
&__results-cta {
|
||||
display: flex;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./QueryReport";
|
||||
351
frontend/pages/queries/edit/EditQueryPage.tsx
Normal file
351
frontend/pages/queries/edit/EditQueryPage.tsx
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { DEFAULT_QUERY } from "utilities/constants";
|
||||
import configAPI from "services/entities/config";
|
||||
import queryAPI from "services/entities/queries";
|
||||
import statusAPI from "services/entities/status";
|
||||
import {
|
||||
IGetQueryResponse,
|
||||
ICreateQueryRequestBody,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
|
||||
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
||||
import MainContent from "components/MainContent";
|
||||
import SidePanelContent from "components/SidePanelContent";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import debounce from "utilities/debounce";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
|
||||
import BackLink from "components/BackLink";
|
||||
import EditQueryForm from "pages/queries/edit/components/EditQueryForm";
|
||||
import { IConfig } from "interfaces/config";
|
||||
|
||||
interface IEditQueryPageProps {
|
||||
router: InjectedRouter;
|
||||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
query: { host_ids: string; team_id?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
const baseClass = "edit-query-page";
|
||||
|
||||
const EditQueryPage = ({
|
||||
router,
|
||||
params: { id: paramsQueryId },
|
||||
location,
|
||||
}: IEditQueryPageProps): JSX.Element => {
|
||||
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
||||
const {
|
||||
currentTeamName: teamNameForQuery,
|
||||
teamIdForApi: apiTeamIdForQuery,
|
||||
} = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
isObserverPlus,
|
||||
isAnyTeamObserverPlus,
|
||||
} = useContext(AppContext);
|
||||
const {
|
||||
selectedOsqueryTable,
|
||||
setSelectedOsqueryTable,
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryDiscardData,
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryPlatforms,
|
||||
setLastEditedQueryDiscardData,
|
||||
} = useContext(QueryContext);
|
||||
const { setConfig } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
|
||||
false
|
||||
);
|
||||
const [
|
||||
showConfirmSaveChangesModal,
|
||||
setShowConfirmSaveChangesModal,
|
||||
] = useState(false);
|
||||
|
||||
const { data: appConfig } = useQuery<IConfig, Error, IConfig>(
|
||||
["config"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
select: (data: IConfig) => data,
|
||||
onSuccess: (data) => {
|
||||
setConfig(data);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// disabled on page load so we can control the number of renders
|
||||
// else it will re-populate the context on occasion
|
||||
const {
|
||||
isLoading: isStoredQueryLoading,
|
||||
data: storedQuery,
|
||||
refetch: refetchStoredQuery,
|
||||
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
||||
["query", queryId],
|
||||
() => queryAPI.load(queryId as number),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => data.query,
|
||||
onSuccess: (returnedQuery) => {
|
||||
setLastEditedQueryId(returnedQuery.id);
|
||||
setLastEditedQueryName(returnedQuery.name);
|
||||
setLastEditedQueryDescription(returnedQuery.description);
|
||||
setLastEditedQueryBody(returnedQuery.query);
|
||||
setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
|
||||
setLastEditedQueryFrequency(returnedQuery.interval);
|
||||
setLastEditedQueryPlatforms(returnedQuery.platform);
|
||||
setLastEditedQueryLoggingType(returnedQuery.logging);
|
||||
setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
|
||||
setLastEditedQueryDiscardData(returnedQuery.discard_data);
|
||||
},
|
||||
onError: (error) => handlePageError(error),
|
||||
}
|
||||
);
|
||||
|
||||
const detectIsFleetQueryRunnable = () => {
|
||||
statusAPI.live_query().catch(() => {
|
||||
setIsLiveQueryRunnable(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
detectIsFleetQueryRunnable();
|
||||
if (!queryId) {
|
||||
setLastEditedQueryId(DEFAULT_QUERY.id);
|
||||
setLastEditedQueryName(DEFAULT_QUERY.name);
|
||||
setLastEditedQueryDescription(DEFAULT_QUERY.description);
|
||||
// Persist lastEditedQueryBody through live query flow instead of resetting to DEFAULT_QUERY.query
|
||||
setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
|
||||
setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
|
||||
setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
|
||||
setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version);
|
||||
setLastEditedQueryPlatforms(DEFAULT_QUERY.platform);
|
||||
setLastEditedQueryDiscardData(DEFAULT_QUERY.discard_data);
|
||||
}
|
||||
}, [queryId]);
|
||||
|
||||
const [isQuerySaving, setIsQuerySaving] = useState(false);
|
||||
const [isQueryUpdating, setIsQueryUpdating] = useState(false);
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
|
||||
// Updates title that shows up on browser tabs
|
||||
useEffect(() => {
|
||||
// e.g., Query details | Discover TLS certificates | Fleet for osquery
|
||||
document.title = `Edit query | ${storedQuery?.name} | Fleet for osquery`;
|
||||
}, [location.pathname, storedQuery?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowOpenSchemaActionText(!isSidebarOpen);
|
||||
}, [isSidebarOpen]);
|
||||
|
||||
const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
|
||||
setIsQuerySaving(true);
|
||||
try {
|
||||
const { query } = await queryAPI.create(formData);
|
||||
router.push(PATHS.EDIT_QUERY(query.id));
|
||||
renderFlash("success", "Query created!");
|
||||
setBackendValidators({});
|
||||
} catch (createError: any) {
|
||||
if (createError.data.errors[0].reason.includes("already exists")) {
|
||||
const teamErrorText =
|
||||
teamNameForQuery && apiTeamIdForQuery !== 0
|
||||
? `the ${teamNameForQuery} team`
|
||||
: "all teams";
|
||||
setBackendValidators({
|
||||
name: `A query with that name already exists for ${teamErrorText}.`,
|
||||
});
|
||||
} else {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Something went wrong creating your query. Please try again."
|
||||
);
|
||||
setBackendValidators({});
|
||||
}
|
||||
} finally {
|
||||
setIsQuerySaving(false);
|
||||
}
|
||||
});
|
||||
|
||||
const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
|
||||
if (!queryId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsQueryUpdating(true);
|
||||
|
||||
const updatedQuery = deepDifference(formData, {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryDiscardData,
|
||||
});
|
||||
|
||||
try {
|
||||
await queryAPI.update(queryId, updatedQuery);
|
||||
renderFlash("success", "Query updated!");
|
||||
refetchStoredQuery(); // Required to compare recently saved query to a subsequent save to the query
|
||||
} catch (updateError: any) {
|
||||
console.error(updateError);
|
||||
if (updateError.data.errors[0].reason.includes("Duplicate")) {
|
||||
renderFlash("error", "A query with this name already exists.");
|
||||
} else {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Something went wrong updating your query. Please try again."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setIsQueryUpdating(false);
|
||||
setShowConfirmSaveChangesModal(false); // Closes conditionally opened modal when discarding previous results
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onOsqueryTableSelect = (tableName: string) => {
|
||||
setSelectedOsqueryTable(tableName);
|
||||
};
|
||||
|
||||
const onCloseSchemaSidebar = () => {
|
||||
setIsSidebarOpen(false);
|
||||
};
|
||||
|
||||
const onOpenSchemaSidebar = () => {
|
||||
setIsSidebarOpen(true);
|
||||
};
|
||||
|
||||
const renderLiveQueryWarning = (): JSX.Element | null => {
|
||||
if (isLiveQueryRunnable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__warning`}>
|
||||
<div className={`${baseClass}__message`}>
|
||||
<p>
|
||||
Fleet is unable to run a live query. Refresh the page or log in
|
||||
again. If this keeps happening please{" "}
|
||||
<CustomLink
|
||||
url="https://github.com/fleetdm/fleet/issues/new/choose"
|
||||
text="file an issue"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Function instead of constant eliminates race condition
|
||||
const backToQueriesPath = () => {
|
||||
return queryId ? PATHS.QUERY(queryId) : PATHS.MANAGE_QUERIES;
|
||||
};
|
||||
|
||||
const showSidebar =
|
||||
isSidebarOpen &&
|
||||
(isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isAnyTeamMaintainerOrTeamAdmin ||
|
||||
isObserverPlus ||
|
||||
isAnyTeamObserverPlus);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}_wrapper`}>
|
||||
<div className={`${baseClass}__form`}>
|
||||
<div className={`${baseClass}__header-links`}>
|
||||
<BackLink
|
||||
text={queryId ? "Back to report" : "Back to queries"}
|
||||
path={backToQueriesPath()}
|
||||
/>
|
||||
</div>
|
||||
<EditQueryForm
|
||||
router={router}
|
||||
saveQuery={saveQuery}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onUpdate={onUpdateQuery}
|
||||
storedQuery={storedQuery}
|
||||
queryIdForEdit={queryId}
|
||||
apiTeamIdForQuery={apiTeamIdForQuery}
|
||||
teamNameForQuery={teamNameForQuery}
|
||||
isStoredQueryLoading={isStoredQueryLoading}
|
||||
showOpenSchemaActionText={showOpenSchemaActionText}
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
renderLiveQueryWarning={renderLiveQueryWarning}
|
||||
backendValidators={backendValidators}
|
||||
isQuerySaving={isQuerySaving}
|
||||
isQueryUpdating={isQueryUpdating}
|
||||
hostId={parseInt(location.query.host_ids as string, 10)}
|
||||
queryReportsDisabled={
|
||||
appConfig?.server_settings.query_reports_disabled
|
||||
}
|
||||
showConfirmSaveChangesModal={showConfirmSaveChangesModal}
|
||||
setShowConfirmSaveChangesModal={setShowConfirmSaveChangesModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MainContent>
|
||||
{showSidebar && (
|
||||
<SidePanelContent>
|
||||
<QuerySidePanel
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
selectedOsqueryTable={selectedOsqueryTable}
|
||||
onClose={onCloseSchemaSidebar}
|
||||
/>
|
||||
</SidePanelContent>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditQueryPage;
|
||||
86
frontend/pages/queries/edit/_styles.scss
Normal file
86
frontend/pages/queries/edit/_styles.scss
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
.edit-query-page {
|
||||
.help-text {
|
||||
margin-top: $pad-small;
|
||||
margin-bottom: $pad-large;
|
||||
font-weight: $regular;
|
||||
font-size: $xx-small;
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
|
||||
.fleet-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
&--frequency {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&--platform {
|
||||
margin-bottom: 0;
|
||||
margin-top: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-options-toggle {
|
||||
font-weight: $xbold;
|
||||
}
|
||||
|
||||
.observer-can-run-wrapper {
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__warning {
|
||||
padding: $pad-medium;
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
background-color: #fff0b9;
|
||||
border: 1px solid #f2c94c;
|
||||
border-radius: $border-radius;
|
||||
margin: 0;
|
||||
margin-top: $pad-large;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_content {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
||||
&__count-spinner {
|
||||
margin-right: $pad-small;
|
||||
}
|
||||
&__page-loading {
|
||||
.loading-spinner {
|
||||
margin: $pad-large 0 0;
|
||||
}
|
||||
}
|
||||
&__page-error {
|
||||
h4 {
|
||||
margin: 0;
|
||||
margin-top: 28px;
|
||||
margin-left: -7px;
|
||||
font-size: $small;
|
||||
|
||||
img {
|
||||
transform: scale(0.5);
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
margin-top: $pad-medium;
|
||||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from "react";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
const baseClass = "save-changes-modal";
|
||||
|
||||
export interface IConfirmSaveChangesModalProps {
|
||||
isUpdating: boolean;
|
||||
onSaveChanges: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onClose: () => void;
|
||||
showChangedSQLCopy?: boolean;
|
||||
}
|
||||
|
||||
const ConfirmSaveChangesModal = ({
|
||||
isUpdating,
|
||||
onSaveChanges,
|
||||
onClose,
|
||||
showChangedSQLCopy = false,
|
||||
}: IConfirmSaveChangesModalProps) => {
|
||||
const warningText = showChangedSQLCopy
|
||||
? "Changing this query's SQL will delete its previous results, since the existing report does not reflect the updated query."
|
||||
: "The changes you are making to this query will delete its previous results.";
|
||||
|
||||
return (
|
||||
<Modal title={"Save changes?"} onExit={onClose}>
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p>{warningText}</p>
|
||||
<p>You cannot undo this action.</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onSaveChanges}
|
||||
variant="brand"
|
||||
className="save-loading"
|
||||
isLoading={isUpdating}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSaveChangesModal;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ConfirmSaveChangesModal";
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import DiscardDataOption from "./DiscardDataOption";
|
||||
|
||||
const meta: Meta<typeof DiscardDataOption> = {
|
||||
title: "Components/DiscardDataOption",
|
||||
component: DiscardDataOption,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DiscardDataOption>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React from "react";
|
||||
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
import DiscardDataOption from "./DiscardDataOption";
|
||||
|
||||
describe("DiscardDataOption component", () => {
|
||||
const selectedLoggingType = "snapshot";
|
||||
const [discardData, setDiscardData] = [false, jest.fn()];
|
||||
|
||||
it("Renders normal help text when the global option is not disabled", () => {
|
||||
render(
|
||||
<DiscardDataOption
|
||||
queryReportsDisabled={false}
|
||||
{...{ selectedLoggingType, discardData, setDiscardData }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Discard data/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the "disabled" help text with tooltip when the global option is disabled', async () => {
|
||||
render(
|
||||
<DiscardDataOption
|
||||
queryReportsDisabled
|
||||
{...{ selectedLoggingType, discardData, setDiscardData }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Discard data/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument();
|
||||
|
||||
await fireEvent.mouseOver(screen.getByText(/globally disabled/));
|
||||
|
||||
expect(screen.getByText(/A Fleet administrator/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Restores normal help text when disabled and then "Edit anyway" is clicked', async () => {
|
||||
render(
|
||||
<DiscardDataOption
|
||||
queryReportsDisabled
|
||||
{...{ selectedLoggingType, discardData, setDiscardData }}
|
||||
/>
|
||||
);
|
||||
|
||||
// disabled
|
||||
expect(screen.getByText(/Discard data/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument();
|
||||
|
||||
// enable
|
||||
await fireEvent.click(screen.getByText(/Edit anyway/));
|
||||
|
||||
// normal text
|
||||
expect(screen.getByText(/Data will still be sent/)).toBeInTheDocument();
|
||||
});
|
||||
it('Renders the info banner when "Differential" logging option is selected', () => {
|
||||
render(
|
||||
<DiscardDataOption
|
||||
selectedLoggingType="differential"
|
||||
queryReportsDisabled={false}
|
||||
{...{ discardData, setDiscardData }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
/setting is ignored when differential logging is enabled. This/
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('Renders the info banner when "Differential (ignore removals)" logging option is selected', () => {
|
||||
render(
|
||||
<DiscardDataOption
|
||||
selectedLoggingType="differential_ignore_removals"
|
||||
queryReportsDisabled={false}
|
||||
{...{ discardData, setDiscardData }}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/setting is ignored when differential logging is enabled. This/
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import Icon from "components/Icon";
|
||||
import InfoBanner from "components/InfoBanner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import { QueryLoggingOption } from "interfaces/schedulable_query";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const baseClass = "discard-data-option";
|
||||
|
||||
interface IDiscardDataOptionProps {
|
||||
queryReportsDisabled: boolean;
|
||||
selectedLoggingType: QueryLoggingOption;
|
||||
discardData: boolean;
|
||||
setDiscardData: (value: boolean) => void;
|
||||
breakHelpText?: boolean;
|
||||
}
|
||||
|
||||
const DiscardDataOption = ({
|
||||
queryReportsDisabled,
|
||||
selectedLoggingType,
|
||||
discardData,
|
||||
setDiscardData,
|
||||
breakHelpText = false,
|
||||
}: IDiscardDataOptionProps) => {
|
||||
const [forceEditDiscardData, setForceEditDiscardData] = useState(false);
|
||||
const disable = queryReportsDisabled && !forceEditDiscardData;
|
||||
|
||||
const renderHelpText = () => (
|
||||
<div className="help-text">
|
||||
{disable ? (
|
||||
<>
|
||||
This setting is ignored because query reports in Fleet have been{" "}
|
||||
<TooltipWrapper
|
||||
// TODO - use JSX once new tooltipwrapper is merged
|
||||
tipContent={
|
||||
"A Fleet administrator can enable query reports under <br />\
|
||||
<b>Organization settings > Advanced options > Disable query reports</b>."
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
{"globally disabled."}
|
||||
</TooltipWrapper>{" "}
|
||||
<Link
|
||||
to={""}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setForceEditDiscardData(true);
|
||||
}}
|
||||
className={`${baseClass}__edit-anyway`}
|
||||
>
|
||||
<>
|
||||
Edit anyway
|
||||
<Icon
|
||||
name="chevron"
|
||||
direction="right"
|
||||
color="core-fleet-blue"
|
||||
size="small"
|
||||
/>
|
||||
</>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The most recent results for each host will not be available in Fleet.
|
||||
{breakHelpText ? <br /> : " "}
|
||||
Data will still be sent to your log destination if <b>
|
||||
automations
|
||||
</b>{" "}
|
||||
are <b>on</b>.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{["differential", "differential_ignore_removals"].includes(
|
||||
selectedLoggingType
|
||||
) && (
|
||||
<InfoBanner color="purple-bold-border">
|
||||
<>
|
||||
The <b>Discard data</b> setting is ignored when differential logging
|
||||
is enabled. This <br />
|
||||
query's results will not be saved in Fleet.
|
||||
</>
|
||||
</InfoBanner>
|
||||
)}
|
||||
<Checkbox
|
||||
name="discardData"
|
||||
onChange={setDiscardData}
|
||||
value={discardData}
|
||||
wrapperClassName={
|
||||
disable ? `${baseClass}__disabled-discard-data-checkbox` : ""
|
||||
}
|
||||
>
|
||||
<b>Discard data</b>
|
||||
</Checkbox>
|
||||
{renderHelpText()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscardDataOption;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
.discard-data-option {
|
||||
.info-banner {
|
||||
margin-bottom: 1.5rem;
|
||||
&__info {
|
||||
line-height: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
&__disabled-discard-data-checkbox {
|
||||
@include disabled;
|
||||
}
|
||||
|
||||
&__edit-anyway {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./DiscardDataOption";
|
||||
|
|
@ -5,7 +5,7 @@ import { createCustomRenderer } from "test/test-utils";
|
|||
import createMockQuery from "__mocks__/queryMock";
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
|
||||
import QueryForm from "./QueryForm";
|
||||
import EditQueryForm from "./EditQueryForm";
|
||||
|
||||
const mockQuery = createMockQuery();
|
||||
const mockRouter = {
|
||||
|
|
@ -20,7 +20,7 @@ const mockRouter = {
|
|||
createPath: jest.fn(),
|
||||
};
|
||||
|
||||
describe("QueryForm - component", () => {
|
||||
describe("EditQueryForm - component", () => {
|
||||
it("disables save button for missing query name", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
|
|
@ -56,7 +56,7 @@ describe("QueryForm - component", () => {
|
|||
});
|
||||
|
||||
render(
|
||||
<QueryForm
|
||||
<EditQueryForm
|
||||
router={mockRouter}
|
||||
queryIdForEdit={1}
|
||||
apiTeamIdForQuery={1}
|
||||
|
|
@ -68,11 +68,12 @@ describe("QueryForm - component", () => {
|
|||
isQueryUpdating={false}
|
||||
saveQuery={jest.fn()}
|
||||
onOsqueryTableSelect={jest.fn()}
|
||||
goToSelectTargets={jest.fn()}
|
||||
onUpdate={jest.fn()}
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
showConfirmSaveChangesModal={false}
|
||||
setShowConfirmSaveChangesModal={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -15,7 +15,11 @@ import PATHS from "router/paths";
|
|||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { addGravatarUrlToResource, secondsToDhms } from "utilities/helpers";
|
||||
import {
|
||||
addGravatarUrlToResource,
|
||||
secondsToDhms,
|
||||
TAGGED_TEMPLATES,
|
||||
} from "utilities/helpers";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
|
||||
|
|
@ -48,10 +52,12 @@ import Spinner from "components/Spinner";
|
|||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import SaveQueryModal from "../SaveQueryModal";
|
||||
import ConfirmSaveChangesModal from "../ConfirmSaveChangesModal";
|
||||
import DiscardDataOption from "../DiscardDataOption";
|
||||
|
||||
const baseClass = "query-form";
|
||||
const baseClass = "edit-query-form";
|
||||
|
||||
interface IQueryFormProps {
|
||||
interface IEditQueryFormProps {
|
||||
router: InjectedRouter;
|
||||
queryIdForEdit: number | null;
|
||||
apiTeamIdForQuery?: number;
|
||||
|
|
@ -63,11 +69,14 @@ interface IQueryFormProps {
|
|||
isQueryUpdating: boolean;
|
||||
saveQuery: (formData: ICreateQueryRequestBody) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
onUpdate: (formData: ICreateQueryRequestBody) => void;
|
||||
onOpenSchemaSidebar: () => void;
|
||||
renderLiveQueryWarning: () => JSX.Element | null;
|
||||
backendValidators: { [key: string]: string };
|
||||
hostId?: number;
|
||||
queryReportsDisabled?: boolean;
|
||||
showConfirmSaveChangesModal: boolean;
|
||||
setShowConfirmSaveChangesModal: (bool: boolean) => void;
|
||||
}
|
||||
|
||||
const validateQuerySQL = (query: string) => {
|
||||
|
|
@ -98,7 +107,7 @@ const customFrequencyOptions = (frequency: number) => {
|
|||
return FREQUENCY_DROPDOWN_OPTIONS;
|
||||
};
|
||||
|
||||
const QueryForm = ({
|
||||
const EditQueryForm = ({
|
||||
router,
|
||||
queryIdForEdit,
|
||||
apiTeamIdForQuery,
|
||||
|
|
@ -110,12 +119,15 @@ const QueryForm = ({
|
|||
isQueryUpdating,
|
||||
saveQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
onUpdate,
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
backendValidators,
|
||||
}: IQueryFormProps): JSX.Element => {
|
||||
hostId,
|
||||
queryReportsDisabled,
|
||||
showConfirmSaveChangesModal,
|
||||
setShowConfirmSaveChangesModal,
|
||||
}: IEditQueryFormProps): JSX.Element => {
|
||||
// Note: The QueryContext values should always be used for any mutable query data such as query name
|
||||
// The storedQuery prop should only be used to access immutable metadata such as author id
|
||||
const {
|
||||
|
|
@ -128,6 +140,7 @@ const QueryForm = ({
|
|||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryDiscardData,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
|
|
@ -136,6 +149,7 @@ const QueryForm = ({
|
|||
setLastEditedQueryPlatforms,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryDiscardData,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const {
|
||||
|
|
@ -169,9 +183,7 @@ const QueryForm = ({
|
|||
const { setCompatiblePlatforms } = platformCompatibility;
|
||||
|
||||
const debounceSQL = useDebouncedCallback((sql: string) => {
|
||||
let valid = true;
|
||||
const { valid: isValidated, errors: newErrors } = validateQuerySQL(sql);
|
||||
valid = isValidated;
|
||||
const { errors: newErrors } = validateQuerySQL(sql);
|
||||
|
||||
setErrors({
|
||||
...newErrors,
|
||||
|
|
@ -194,17 +206,14 @@ const QueryForm = ({
|
|||
}
|
||||
}, [lastEditedQueryFrequency, isInitialFrequency]);
|
||||
|
||||
const hasTeamMaintainerPermissions = savedQueryMode
|
||||
? isAnyTeamMaintainerOrTeamAdmin &&
|
||||
storedQuery &&
|
||||
currentUser &&
|
||||
storedQuery.author_id === currentUser.id
|
||||
: isAnyTeamMaintainerOrTeamAdmin;
|
||||
|
||||
const toggleSaveQueryModal = () => {
|
||||
setShowSaveQueryModal(!showSaveQueryModal);
|
||||
};
|
||||
|
||||
const toggleConfirmSaveChangesModal = () => {
|
||||
setShowConfirmSaveChangesModal(!showConfirmSaveChangesModal);
|
||||
};
|
||||
|
||||
const onLoad = (editor: IAceEditor) => {
|
||||
editor.setOptions({
|
||||
enableLinking: true,
|
||||
|
|
@ -399,6 +408,7 @@ const QueryForm = ({
|
|||
platform: lastEditedQueryPlatforms,
|
||||
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
||||
logging: lastEditedQueryLoggingType,
|
||||
discard_data: lastEditedQueryDiscardData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -576,7 +586,12 @@ const QueryForm = ({
|
|||
<Button
|
||||
className={`${baseClass}__run`}
|
||||
variant="blue-green"
|
||||
onClick={goToSelectTargets}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
PATHS.LIVE_QUERY(queryIdForEdit) +
|
||||
TAGGED_TEMPLATES.queryByHostRoute(hostId)
|
||||
);
|
||||
}}
|
||||
>
|
||||
Live query
|
||||
</Button>
|
||||
|
|
@ -587,6 +602,28 @@ const QueryForm = ({
|
|||
|
||||
const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer;
|
||||
|
||||
const currentlySavingQueryResults =
|
||||
storedQuery &&
|
||||
!storedQuery.discard_data &&
|
||||
!["differential", "differential_ignore_removals"].includes(
|
||||
storedQuery.logging
|
||||
);
|
||||
const changedSQL = storedQuery && lastEditedQueryBody !== storedQuery.query;
|
||||
const changedLoggingToDifferential = [
|
||||
"differential",
|
||||
"differential_ignore_removals",
|
||||
].includes(lastEditedQueryLoggingType);
|
||||
|
||||
const enabledDiscardData =
|
||||
storedQuery && lastEditedQueryDiscardData && !storedQuery.discard_data;
|
||||
|
||||
const confirmChanges =
|
||||
currentlySavingQueryResults &&
|
||||
(changedSQL || changedLoggingToDifferential || enabledDiscardData);
|
||||
|
||||
const showChangedSQLCopy =
|
||||
changedSQL && !changedLoggingToDifferential && !enabledDiscardData;
|
||||
|
||||
// Global admin, any maintainer, any observer+ on new query
|
||||
const renderEditableQueryForm = () => {
|
||||
// Save disabled for team maintainer/admins viewing global queries
|
||||
|
|
@ -618,7 +655,9 @@ const QueryForm = ({
|
|||
onLoad={onLoad}
|
||||
wrapperClassName={`${baseClass}__text-editor-wrapper`}
|
||||
onChange={onChangeQuery}
|
||||
handleSubmit={promptSaveQuery}
|
||||
handleSubmit={
|
||||
confirmChanges ? toggleConfirmSaveChangesModal : promptSaveQuery
|
||||
}
|
||||
wrapEnabled
|
||||
focus={!savedQueryMode}
|
||||
/>
|
||||
|
|
@ -635,10 +674,11 @@ const QueryForm = ({
|
|||
placeholder={"Every day"}
|
||||
value={lastEditedQueryFrequency}
|
||||
label={"Frequency"}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
wrapperClassName={`${baseClass}__form-field form-field--frequency`}
|
||||
/>
|
||||
If automations are on, this is how often your query collects
|
||||
data.
|
||||
<div className="help-text">
|
||||
This is how often your query collects data.
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__observers-can-run`}>
|
||||
<Checkbox
|
||||
|
|
@ -646,18 +686,18 @@ const QueryForm = ({
|
|||
onChange={(value: boolean) =>
|
||||
setLastEditedQueryObserverCanRun(value)
|
||||
}
|
||||
wrapperClassName={`${baseClass}__query-observer-can-run-wrapper`}
|
||||
wrapperClassName={"observer-can-run-wrapper"}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
<div className="help-text">
|
||||
Users with the observer role will be able to run this query on
|
||||
hosts where they have access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className={baseClass}
|
||||
className={"advanced-options-toggle"}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
|
|
@ -672,8 +712,12 @@ const QueryForm = ({
|
|||
onChange={onChangeSelectPlatformOptions}
|
||||
value={lastEditedQueryPlatforms}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
wrapperClassName={`${baseClass}__form-field form-field--platform`}
|
||||
/>
|
||||
<div className="help-text">
|
||||
By default, your query collects data on all compatible
|
||||
platforms.
|
||||
</div>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={onChangeMinOsqueryVersionOptions}
|
||||
|
|
@ -690,6 +734,14 @@ const QueryForm = ({
|
|||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
{queryReportsDisabled !== undefined && (
|
||||
<DiscardDataOption
|
||||
selectedLoggingType={lastEditedQueryLoggingType}
|
||||
discardData={lastEditedQueryDiscardData}
|
||||
setDiscardData={setLastEditedQueryDiscardData}
|
||||
queryReportsDisabled={queryReportsDisabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -711,7 +763,7 @@ const QueryForm = ({
|
|||
Save as new
|
||||
</Button>
|
||||
)}
|
||||
<div className="query-form__button-wrap--save-query-button">
|
||||
<div className={`${baseClass}__button-wrap--save-query-button`}>
|
||||
<div
|
||||
data-tip
|
||||
data-for="save-query-button"
|
||||
|
|
@ -721,7 +773,11 @@ const QueryForm = ({
|
|||
<Button
|
||||
className="save-loading"
|
||||
variant="brand"
|
||||
onClick={promptSaveQuery()}
|
||||
onClick={
|
||||
confirmChanges
|
||||
? toggleConfirmSaveChangesModal
|
||||
: promptSaveQuery()
|
||||
}
|
||||
// Button disabled for team maintainer/admins viewing global queries
|
||||
disabled={
|
||||
disableSavePermissionDenied || disableSaveFormErrors
|
||||
|
|
@ -750,7 +806,12 @@ const QueryForm = ({
|
|||
<Button
|
||||
className={`${baseClass}__run`}
|
||||
variant="blue-green"
|
||||
onClick={goToSelectTargets}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
PATHS.LIVE_QUERY(queryIdForEdit) +
|
||||
TAGGED_TEMPLATES.queryByHostRoute(hostId)
|
||||
);
|
||||
}}
|
||||
>
|
||||
Live query
|
||||
</Button>
|
||||
|
|
@ -764,6 +825,15 @@ const QueryForm = ({
|
|||
toggleSaveQueryModal={toggleSaveQueryModal}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={isQuerySaving}
|
||||
queryReportsDisabled={queryReportsDisabled}
|
||||
/>
|
||||
)}
|
||||
{showConfirmSaveChangesModal && (
|
||||
<ConfirmSaveChangesModal
|
||||
onSaveChanges={promptSaveQuery()}
|
||||
isUpdating={isQueryUpdating}
|
||||
onClose={toggleConfirmSaveChangesModal}
|
||||
showChangedSQLCopy={showChangedSQLCopy}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -791,4 +861,4 @@ const QueryForm = ({
|
|||
return renderEditableQueryForm();
|
||||
};
|
||||
|
||||
export default QueryForm;
|
||||
export default EditQueryForm;
|
||||
|
|
@ -1,13 +1,8 @@
|
|||
.query-form {
|
||||
.edit-query-form {
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
font-size: $x-small;
|
||||
|
||||
.query-page__warning {
|
||||
margin: 0;
|
||||
margin-top: $pad-large;
|
||||
}
|
||||
|
||||
.form-field--input {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -51,7 +46,7 @@
|
|||
.query-name-wrapper {
|
||||
display: flex;
|
||||
|
||||
&:not(.query-form--editing) {
|
||||
&:not(.edit-query-form--editing) {
|
||||
textarea:hover {
|
||||
cursor: pointer;
|
||||
color: $core-vibrant-blue;
|
||||
|
|
@ -62,7 +57,7 @@
|
|||
top: 13px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.query-form__query-name,
|
||||
.edit-query-form__query-name,
|
||||
.input-sizer::after {
|
||||
font-size: $large;
|
||||
}
|
||||
|
|
@ -75,7 +70,7 @@
|
|||
.query-description-wrapper {
|
||||
display: flex;
|
||||
padding-top: $pad-small;
|
||||
&:not(.query-form--editing) {
|
||||
&:not(.edit-query-form--editing) {
|
||||
textarea:hover {
|
||||
cursor: pointer;
|
||||
color: $core-vibrant-blue;
|
||||
|
|
@ -166,26 +161,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__advanced-options {
|
||||
margin-top: $pad-medium;
|
||||
}
|
||||
|
||||
&__query-observer-can-run-wrapper {
|
||||
margin: 0;
|
||||
margin-top: $pad-large;
|
||||
font-weight: $bold !important; // override checkbox default
|
||||
|
||||
& + p {
|
||||
margin: 0;
|
||||
margin-top: $pad-small;
|
||||
}
|
||||
|
||||
.fleet-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
margin: 0;
|
||||
margin-top: $pad-large;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./EditQueryForm";
|
||||
|
|
@ -23,6 +23,7 @@ import {
|
|||
ISchedulableQuery,
|
||||
QueryLoggingOption,
|
||||
} from "interfaces/schedulable_query";
|
||||
import DiscardDataOption from "../DiscardDataOption";
|
||||
|
||||
const baseClass = "save-query-modal";
|
||||
export interface ISaveQueryModalProps {
|
||||
|
|
@ -33,6 +34,7 @@ export interface ISaveQueryModalProps {
|
|||
toggleSaveQueryModal: () => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
existingQuery?: ISchedulableQuery;
|
||||
queryReportsDisabled?: boolean;
|
||||
}
|
||||
|
||||
const validateQueryName = (name: string) => {
|
||||
|
|
@ -54,6 +56,7 @@ const SaveQueryModal = ({
|
|||
toggleSaveQueryModal,
|
||||
backendValidators,
|
||||
existingQuery,
|
||||
queryReportsDisabled,
|
||||
}: ISaveQueryModalProps): JSX.Element => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
|
@ -73,6 +76,7 @@ const SaveQueryModal = ({
|
|||
setSelectedLoggingType,
|
||||
] = useState<QueryLoggingOption>(existingQuery?.logging ?? "snapshot");
|
||||
const [observerCanRun, setObserverCanRun] = useState(false);
|
||||
const [discardData, setDiscardData] = useState(false);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
|
|
@ -108,6 +112,7 @@ const SaveQueryModal = ({
|
|||
description,
|
||||
interval: selectedFrequency,
|
||||
observer_can_run: observerCanRun,
|
||||
discard_data: discardData,
|
||||
platform: selectedPlatformOptions,
|
||||
min_osquery_version: selectedMinOsqueryVersionOptions,
|
||||
logging: selectedLoggingType,
|
||||
|
|
@ -141,116 +146,122 @@ const SaveQueryModal = ({
|
|||
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={toggleSaveQueryModal}>
|
||||
<>
|
||||
<form
|
||||
onSubmit={onClickSaveQuery}
|
||||
className={baseClass}
|
||||
autoComplete="off"
|
||||
<form
|
||||
onSubmit={onClickSaveQuery}
|
||||
className={baseClass}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
autofocus
|
||||
ignore1password
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={(value: number) => {
|
||||
setSelectedFrequency(value);
|
||||
}}
|
||||
placeholder={"Every hour"}
|
||||
value={selectedFrequency}
|
||||
label="Frequency"
|
||||
wrapperClassName={`${baseClass}__form-field form-field--frequency`}
|
||||
/>
|
||||
<div className="help-text">
|
||||
This is how often your query collects data.
|
||||
</div>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={"observer-can-run-wrapper"}
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
autofocus
|
||||
ignore1password
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={(value: number) => {
|
||||
setSelectedFrequency(value);
|
||||
}}
|
||||
placeholder={"Every hour"}
|
||||
value={selectedFrequency}
|
||||
label="Frequency"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
/>
|
||||
<p className="help-text">
|
||||
If automations are on, this is how often your query collects data.
|
||||
</p>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={`${baseClass}__observer-can-run-wrapper`}
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<div className="help-text">
|
||||
Users with the Observer role will be able to run this query as a live
|
||||
query.
|
||||
</div>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className={"advanced-options-toggle"}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<>
|
||||
<Dropdown
|
||||
options={SCHEDULE_PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platforms"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={selectedPlatformOptions}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field form-field--platform`}
|
||||
/>
|
||||
<div className="help-text">
|
||||
By default, your query collects data on all compatible platforms.
|
||||
</div>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={setSelectedMinOsqueryVersionOptions}
|
||||
placeholder="Select"
|
||||
value={selectedMinOsqueryVersionOptions}
|
||||
label="Minimum osquery version"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={LOGGING_TYPE_OPTIONS}
|
||||
onChange={setSelectedLoggingType}
|
||||
placeholder="Select"
|
||||
value={selectedLoggingType}
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
{queryReportsDisabled !== undefined && (
|
||||
<DiscardDataOption
|
||||
{...{
|
||||
queryReportsDisabled,
|
||||
selectedLoggingType,
|
||||
discardData,
|
||||
setDiscardData,
|
||||
breakHelpText: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="save-query-loading"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p className="help-text">
|
||||
Users with the Observer role will be able to run this query as a
|
||||
live query.
|
||||
</p>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className={`${baseClass}__advanced-options-toggle`}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<>
|
||||
<Dropdown
|
||||
options={SCHEDULE_PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platforms"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={selectedPlatformOptions}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<p className="help-text">
|
||||
If automations are turned on, your query collects data on
|
||||
compatible platforms.
|
||||
<br />
|
||||
If you want more control, override platforms.
|
||||
</p>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={setSelectedMinOsqueryVersionOptions}
|
||||
placeholder="Select"
|
||||
value={selectedMinOsqueryVersionOptions}
|
||||
label="Minimum osquery version"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={LOGGING_TYPE_OPTIONS}
|
||||
onChange={setSelectedLoggingType}
|
||||
placeholder="Select"
|
||||
value={selectedLoggingType}
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="save-query-loading"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={toggleSaveQueryModal} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={toggleSaveQueryModal} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
1
frontend/pages/queries/edit/index.ts
Normal file
1
frontend/pages/queries/edit/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./EditQueryPage";
|
||||
219
frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
Normal file
219
frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import React, { useState, useEffect, useContext, useCallback } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { LIVE_QUERY_STEPS, DEFAULT_QUERY } from "utilities/constants";
|
||||
import queryAPI from "services/entities/queries";
|
||||
import hostAPI from "services/entities/hosts";
|
||||
import statusAPI from "services/entities/status";
|
||||
import { IHost, IHostResponse } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import {
|
||||
IGetQueryResponse,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
|
||||
import MainContent from "components/MainContent";
|
||||
import SelectTargets from "components/LiveQuery/SelectTargets";
|
||||
|
||||
import RunQuery from "pages/queries/live/screens/RunQuery";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
|
||||
interface IRunQueryPageProps {
|
||||
router: InjectedRouter;
|
||||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
query: { host_ids: string; team_id?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
const baseClass = "run-query-page";
|
||||
|
||||
const RunQueryPage = ({
|
||||
router,
|
||||
params: { id: paramsQueryId },
|
||||
location,
|
||||
}: IRunQueryPageProps): JSX.Element => {
|
||||
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
|
||||
const {
|
||||
currentTeamName: teamNameForQuery,
|
||||
teamIdForApi: apiTeamIdForQuery,
|
||||
} = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const handlePageError = useErrorHandler();
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
isObserverPlus,
|
||||
isAnyTeamObserverPlus,
|
||||
} = useContext(AppContext);
|
||||
const {
|
||||
selectedQueryTargets,
|
||||
setSelectedQueryTargets,
|
||||
selectedQueryTargetsByType,
|
||||
setSelectedQueryTargetsByType,
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryPlatforms,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
|
||||
const [step, setStep] = useState(LIVE_QUERY_STEPS[1]);
|
||||
const [targetedHosts, setTargetedHosts] = useState<IHost[]>(
|
||||
selectedQueryTargetsByType.hosts
|
||||
);
|
||||
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>(
|
||||
selectedQueryTargetsByType.labels
|
||||
);
|
||||
const [targetedTeams, setTargetedTeams] = useState<ITeam[]>(
|
||||
selectedQueryTargetsByType.teams
|
||||
);
|
||||
const [targetsTotalCount, setTargetsTotalCount] = useState(0);
|
||||
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
|
||||
|
||||
// disabled on page load so we can control the number of renders
|
||||
// else it will re-populate the context on occasion
|
||||
const { data: storedQuery } = useQuery<
|
||||
IGetQueryResponse,
|
||||
Error,
|
||||
ISchedulableQuery
|
||||
>(["query", queryId], () => queryAPI.load(queryId as number), {
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => data.query,
|
||||
onSuccess: (returnedQuery) => {
|
||||
setLastEditedQueryId(returnedQuery.id);
|
||||
setLastEditedQueryName(returnedQuery.name);
|
||||
setLastEditedQueryDescription(returnedQuery.description);
|
||||
setLastEditedQueryBody(returnedQuery.query);
|
||||
setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
|
||||
setLastEditedQueryFrequency(returnedQuery.interval);
|
||||
setLastEditedQueryPlatforms(returnedQuery.platform);
|
||||
setLastEditedQueryLoggingType(returnedQuery.logging);
|
||||
setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
|
||||
},
|
||||
onError: (error) => handlePageError(error),
|
||||
});
|
||||
|
||||
useQuery<IHostResponse, Error, IHost>(
|
||||
"hostFromURL",
|
||||
() =>
|
||||
hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
|
||||
{
|
||||
enabled: !!location.query.host_ids && !queryParamHostsAdded,
|
||||
select: (data: IHostResponse) => data.host,
|
||||
onSuccess: (host) => {
|
||||
setTargetedHosts((prevHosts) =>
|
||||
prevHosts.filter((h) => h.id !== host.id).concat(host)
|
||||
);
|
||||
const targets = selectedQueryTargets;
|
||||
host.target_type = "hosts";
|
||||
targets.push(host);
|
||||
setSelectedQueryTargets([...targets]);
|
||||
if (!queryParamHostsAdded) {
|
||||
setQueryParamHostsAdded(true);
|
||||
}
|
||||
router.replace(location.pathname);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const detectIsFleetQueryRunnable = () => {
|
||||
statusAPI.live_query().catch(() => {
|
||||
setIsLiveQueryRunnable(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
detectIsFleetQueryRunnable();
|
||||
}, [queryId]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedQueryTargetsByType({
|
||||
hosts: targetedHosts,
|
||||
labels: targetedLabels,
|
||||
teams: targetedTeams,
|
||||
});
|
||||
}, [targetedLabels, targetedHosts, targetedTeams]);
|
||||
|
||||
console.log(
|
||||
"LiveQueryPage.tsx: selectedQueryTargetsByType",
|
||||
selectedQueryTargetsByType
|
||||
);
|
||||
|
||||
// Updates title that shows up on browser tabs
|
||||
useEffect(() => {
|
||||
// e.g., Run live query | Discover TLS certificates | Fleet for osquery
|
||||
document.title = `Run live query | ${storedQuery?.name} | Fleet for osquery`;
|
||||
}, [location.pathname, storedQuery?.name]);
|
||||
|
||||
const goToQueryEditor = useCallback(
|
||||
() =>
|
||||
queryId
|
||||
? router.push(PATHS.EDIT_QUERY(queryId))
|
||||
: router.push(PATHS.NEW_QUERY()),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderScreen = () => {
|
||||
const step1Props = {
|
||||
baseClass,
|
||||
queryId,
|
||||
selectedTargets: selectedQueryTargets,
|
||||
targetedHosts,
|
||||
targetedLabels,
|
||||
targetedTeams,
|
||||
targetsTotalCount,
|
||||
goToQueryEditor,
|
||||
goToRunQuery: () => setStep(LIVE_QUERY_STEPS[2]),
|
||||
setSelectedTargets: setSelectedQueryTargets,
|
||||
setTargetedHosts,
|
||||
setTargetedLabels,
|
||||
setTargetedTeams,
|
||||
setTargetsTotalCount,
|
||||
};
|
||||
|
||||
const step2Props = {
|
||||
queryId,
|
||||
selectedTargets: selectedQueryTargets,
|
||||
storedQuery,
|
||||
setSelectedTargets: setSelectedQueryTargets,
|
||||
goToQueryEditor,
|
||||
targetsTotalCount,
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
case LIVE_QUERY_STEPS[2]:
|
||||
return <RunQuery {...step2Props} />;
|
||||
default:
|
||||
return <SelectTargets {...step1Props} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}_wrapper`}>{renderScreen()}</div>
|
||||
</MainContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunQueryPage;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.query-page {
|
||||
.run-query-page {
|
||||
.body-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -9,61 +9,6 @@
|
|||
min-height: 400px;
|
||||
}
|
||||
|
||||
&__warning {
|
||||
padding: $pad-medium;
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
background-color: #fff0b9;
|
||||
border: 1px solid #f2c94c;
|
||||
border-radius: $border-radius;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__observer-query-view {
|
||||
width: 90%;
|
||||
max-width: 1060px;
|
||||
margin: 0 auto;
|
||||
color: $core-fleet-black;
|
||||
|
||||
h1 {
|
||||
font-size: $medium;
|
||||
}
|
||||
p {
|
||||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__observer-query-details {
|
||||
padding: 0 2rem;
|
||||
|
||||
h1 {
|
||||
margin: $pad-large 0;
|
||||
font-size: $large;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
|
||||
.sql-button {
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__query-preview {
|
||||
margin-top: 15px;
|
||||
|
||||
.fleet-ace__label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_content {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
|
@ -172,10 +117,4 @@
|
|||
font-size: $x-small;
|
||||
}
|
||||
}
|
||||
|
||||
.targets-input {
|
||||
.input-icon-field__icon {
|
||||
top: 34px; // Override styling to include label header
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/pages/queries/live/LiveQueryPage/index.ts
Normal file
1
frontend/pages/queries/live/LiveQueryPage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./LiveQueryPage";
|
||||
|
|
@ -15,7 +15,7 @@ import { ICampaign, ICampaignState } from "interfaces/campaign";
|
|||
import { IQuery } from "interfaces/query";
|
||||
import { ITarget } from "interfaces/target";
|
||||
|
||||
import QueryResults from "../components/QueryResults";
|
||||
import QueryResults from "../../edit/components/QueryResults";
|
||||
|
||||
interface IRunQueryProps {
|
||||
storedQuery: IQuery | undefined;
|
||||
|
|
@ -36,7 +36,9 @@ import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
|
|||
import NoAccessPage from "pages/NoAccessPage";
|
||||
import PackComposerPage from "pages/packs/PackComposerPage";
|
||||
import PolicyPage from "pages/policies/PolicyPage";
|
||||
import QueryPage from "pages/queries/QueryPage";
|
||||
import QueryDetailsPage from "pages/queries/details/QueryDetailsPage";
|
||||
import LiveQueryPage from "pages/queries/live/LiveQueryPage";
|
||||
import EditQueryPage from "pages/queries/edit/EditQueryPage";
|
||||
import RegistrationPage from "pages/RegistrationPage";
|
||||
import ResetPasswordPage from "pages/ResetPasswordPage";
|
||||
import MDMAppleSSOPage from "pages/MDMAppleSSOPage";
|
||||
|
|
@ -223,9 +225,16 @@ const routes = (
|
|||
<IndexRedirect to="manage" />
|
||||
<Route path="manage" component={ManageQueriesPage} />
|
||||
<Route component={AuthAnyMaintainerAdminObserverPlusRoutes}>
|
||||
<Route path="new" component={QueryPage} />
|
||||
<Route path="new">
|
||||
<IndexRoute component={EditQueryPage} />
|
||||
<Route path="live" component={LiveQueryPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path=":id">
|
||||
<IndexRoute component={QueryDetailsPage} />
|
||||
<Route path="edit" component={EditQueryPage} />
|
||||
<Route path="live" component={LiveQueryPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={QueryPage} />
|
||||
</Route>
|
||||
<Route path="policies">
|
||||
<IndexRedirect to="manage" />
|
||||
|
|
|
|||
|
|
@ -53,6 +53,16 @@ export default {
|
|||
return `${URL_PREFIX}/labels/${labelId}`;
|
||||
},
|
||||
EDIT_QUERY: (queryId: number, teamId?: number): string => {
|
||||
return `${URL_PREFIX}/queries/${queryId}/edit${
|
||||
teamId ? `?team_id=${teamId}` : ""
|
||||
}`;
|
||||
},
|
||||
LIVE_QUERY: (queryId: number | null, teamId?: number): string => {
|
||||
return `${URL_PREFIX}/queries/${queryId || "new"}/live${
|
||||
teamId ? `?team_id=${teamId}` : ""
|
||||
}`;
|
||||
},
|
||||
QUERY: (queryId: number, teamId?: number): string => {
|
||||
return `${URL_PREFIX}/queries/${queryId}${
|
||||
teamId ? `?team_id=${teamId}` : ""
|
||||
}`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest, { getError } from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { ISelectedTargets } from "interfaces/target";
|
||||
import { ISelectedTargetsForApi } from "interfaces/target";
|
||||
import { AxiosResponse } from "axios";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
|
|
@ -52,12 +52,12 @@ export default {
|
|||
}: {
|
||||
query: string;
|
||||
queryId: number | null;
|
||||
selected: ISelectedTargets;
|
||||
selected: ISelectedTargetsForApi;
|
||||
}) => {
|
||||
const { RUN_QUERY } = endpoints;
|
||||
const { LIVE_QUERY } = endpoints;
|
||||
|
||||
try {
|
||||
const { campaign } = await sendRequest("POST", RUN_QUERY, {
|
||||
const { campaign } = await sendRequest("POST", LIVE_QUERY, {
|
||||
query,
|
||||
query_id: queryId,
|
||||
selected,
|
||||
|
|
|
|||
46
frontend/services/entities/query_report.ts
Normal file
46
frontend/services/entities/query_report.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
export interface ISortOption {
|
||||
key: string;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
export interface ILoadQueryReportOptions {
|
||||
id: number;
|
||||
sortBy: ISortOption[];
|
||||
}
|
||||
|
||||
const getSortParams = (sortOptions?: ISortOption[]) => {
|
||||
if (sortOptions === undefined || sortOptions.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const sortItem = sortOptions[0];
|
||||
return {
|
||||
order_key: sortItem.key,
|
||||
order_direction: sortItem.direction,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
load: ({ id, sortBy }: ILoadQueryReportOptions) => {
|
||||
const sortParams = getSortParams(sortBy);
|
||||
|
||||
const { QUERIES } = endpoints;
|
||||
|
||||
const queryParams = {
|
||||
order_key: sortParams.order_key,
|
||||
order_direction: sortParams.order_direction,
|
||||
};
|
||||
|
||||
const queryString = buildQueryStringFromParams(queryParams);
|
||||
|
||||
const endpoint = `${QUERIES}/${id}/report`;
|
||||
const path = `${endpoint}?${queryString}`;
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import { IHost } from "interfaces/host";
|
||||
import { ISelectedTargets, ITargetsAPIResponse } from "interfaces/target";
|
||||
import { ISelectedTargetsForApi, ITargetsAPIResponse } from "interfaces/target";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import appendTargetTypeToTargets from "utilities/append_target_type_to_targets";
|
||||
|
||||
interface ITargetsProps {
|
||||
query?: string;
|
||||
queryId?: number | null;
|
||||
selected: ISelectedTargets;
|
||||
selected: ISelectedTargetsForApi;
|
||||
}
|
||||
|
||||
const defaultSelected = {
|
||||
|
|
@ -29,7 +29,7 @@ export interface ITargetsSearchResponse {
|
|||
|
||||
export interface ITargetsCountParams {
|
||||
query_id?: number | null;
|
||||
selected: ISelectedTargets | null;
|
||||
selected: ISelectedTargetsForApi | null;
|
||||
}
|
||||
|
||||
export interface ITargetsCountResponse {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
|||
"queries/7": RESPONSES.globalQuery6,
|
||||
"queries/8": RESPONSES.teamQuery2,
|
||||
"queries?team_id=13": RESPONSES.teamQueries,
|
||||
"queries/113/report?order_key=host_name&order_direction=asc":
|
||||
RESPONSES.queryReport,
|
||||
},
|
||||
POST: {
|
||||
// request body is ISelectedTargets
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,11 +9,11 @@ $max-width: 2560px;
|
|||
@content;
|
||||
}
|
||||
} @else if ($size == ltdesktop) {
|
||||
@media (max-width: $desktop-width - 1) {
|
||||
@media (max-width: ($desktop-width - 1)) {
|
||||
@content;
|
||||
}
|
||||
} @else if ($size == desktop) {
|
||||
@media (min-width: $medium-width + 1) {
|
||||
@media (min-width: ($medium-width + 1)) {
|
||||
@content;
|
||||
}
|
||||
} @else if ($size == smalldesk) {
|
||||
|
|
@ -108,3 +108,10 @@ $max-width: 2560px;
|
|||
list-style-position: inside;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin disabled {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.5);
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,12 +96,17 @@ export const MIN_OSQUERY_VERSION_OPTIONS = [
|
|||
{ label: "1.8.1 +", value: "1.8.1" },
|
||||
];
|
||||
|
||||
export const QUERIES_PAGE_STEPS = {
|
||||
export const LIVE_POLICY_STEPS = {
|
||||
1: "EDITOR",
|
||||
2: "TARGETS",
|
||||
3: "RUN",
|
||||
};
|
||||
|
||||
export const LIVE_QUERY_STEPS = {
|
||||
1: "TARGETS",
|
||||
2: "RUN",
|
||||
};
|
||||
|
||||
export const DEFAULT_QUERY: ISchedulableQuery = {
|
||||
description: "",
|
||||
name: "",
|
||||
|
|
@ -109,6 +114,7 @@ export const DEFAULT_QUERY: ISchedulableQuery = {
|
|||
id: 0,
|
||||
interval: 0,
|
||||
observer_can_run: false,
|
||||
discard_data: false,
|
||||
platform: "",
|
||||
min_osquery_version: "",
|
||||
automations_enabled: false,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default {
|
|||
PERFORM_REQUIRED_PASSWORD_RESET: `/${API_VERSION}/fleet/perform_required_password_reset`,
|
||||
QUERIES: `/${API_VERSION}/fleet/queries`,
|
||||
RESET_PASSWORD: `/${API_VERSION}/fleet/reset_password`,
|
||||
RUN_QUERY: `/${API_VERSION}/fleet/queries/run`,
|
||||
LIVE_QUERY: `/${API_VERSION}/fleet/queries/run`,
|
||||
SCHEDULE_QUERY: `/${API_VERSION}/fleet/packs/schedule`,
|
||||
SCHEDULED_QUERIES: (packId: number): string => {
|
||||
return `/${API_VERSION}/fleet/packs/${packId}/scheduled`;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const generateCSVFilename = (descriptor: string) => {
|
|||
return `${descriptor} (${format(new Date(), "MM-dd-yy hh-mm-ss")}).csv`;
|
||||
};
|
||||
|
||||
// Query results and query errors
|
||||
// Live query results, live query errors, and query report
|
||||
export const generateCSVQueryResults = (
|
||||
rows: Row[],
|
||||
filename: string,
|
||||
|
|
@ -35,7 +35,7 @@ export const generateCSVQueryResults = (
|
|||
);
|
||||
};
|
||||
|
||||
// Policy results only
|
||||
// Live policy results only
|
||||
export const generateCSVPolicyResults = (
|
||||
rows: { host: string; status: string }[],
|
||||
filename: string
|
||||
|
|
@ -45,7 +45,7 @@ export const generateCSVPolicyResults = (
|
|||
});
|
||||
};
|
||||
|
||||
// Policy errors only
|
||||
// Live policy errors only
|
||||
export const generateCSVPolicyErrors = (
|
||||
rows: ICampaignError[],
|
||||
filename: string
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import {
|
|||
} from "interfaces/scheduled_query";
|
||||
import {
|
||||
ISelectTargetsEntity,
|
||||
ISelectedTargets,
|
||||
ISelectedTargetsForApi,
|
||||
IPackTargets,
|
||||
} from "interfaces/target";
|
||||
import { ITeam, ITeamSummary } from "interfaces/team";
|
||||
|
|
@ -258,7 +258,7 @@ const formatLabelResponse = (response: any): ILabel[] => {
|
|||
|
||||
export const formatSelectedTargetsForApi = (
|
||||
selectedTargets: ISelectTargetsEntity[]
|
||||
): ISelectedTargets => {
|
||||
): ISelectedTargetsForApi => {
|
||||
const targets = selectedTargets || [];
|
||||
// TODO: can flatMap be removed?
|
||||
const hostIds = flatMap(targets, filterTarget("hosts"));
|
||||
|
|
@ -910,6 +910,12 @@ export const getSoftwareBundleTooltipMarkup = (bundle: string) => {
|
|||
`;
|
||||
};
|
||||
|
||||
export const TAGGED_TEMPLATES = {
|
||||
queryByHostRoute: (hostId: number | undefined | null) => {
|
||||
return `${hostId ? `?host_ids=${hostId}` : ""}`;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
addGravatarUrlToResource,
|
||||
formatConfigDataForServer,
|
||||
|
|
@ -945,4 +951,5 @@ export default {
|
|||
syntaxHighlight,
|
||||
normalizeEmptyValues,
|
||||
wrapFleetHelper,
|
||||
TAGGED_TEMPLATES,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -464,6 +464,7 @@ var hostRefs = []string{
|
|||
"host_disk_encryption_keys",
|
||||
"host_software_installed_paths",
|
||||
"host_script_results",
|
||||
"query_results",
|
||||
}
|
||||
|
||||
// NOTE: The following tables are explicity excluded from hostRefs list and accordingly are not
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue