mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
7765 combined schedules and queries (#13037)
Issue #7765 – Combine the query and schedule features to provide a single interface for creating, scheduling, and tweaking queries at the global and team level. Co-authored-by: Juan Fernandez <juan@fleetdm.com> Co-authored-by: Lucas Manuel Rodriguez <lucarodriguez@gmail.com> Co-authored-by: Rachel Perkins <rachel.elysia.perkins@gmail.com> Co-authored-by: Jacob Shandling <jacobshandling@gmail.com>
This commit is contained in:
commit
dba896f901
176 changed files with 7173 additions and 4335 deletions
1
changes/12636-merge-schedule-into-queries
Normal file
1
changes/12636-merge-schedule-into-queries
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Merged all functionality of the Schedule page into the Queries page
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
- The `osquery/config` endpoint should include scheduled queries for the host's team stored in the
|
||||
`queries` table.
|
||||
1
changes/12645-manage-query-automations
Normal file
1
changes/12645-manage-query-automations
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Users able to manage schedulable queries (new feature) with automations modal
|
||||
1
changes/12646-new-query-editor
Normal file
1
changes/12646-new-query-editor
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Query editor includes frequency and other advanced options
|
||||
1
changes/12646-update-save-query-modal
Normal file
1
changes/12646-update-save-query-modal
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Update the save query modal to include scheduling-related fields.
|
||||
1
changes/12999-platforms-column
Normal file
1
changes/12999-platforms-column
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Update the "Platforms" column to the more explicit "Compatible with"
|
||||
1
changes/7765-combine-schedules-and-queries
Normal file
1
changes/7765-combine-schedules-and-queries
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Combine the query and schedule features to provide a single interface for creating, scheduling, and tweaking queries at the global and team level.
|
||||
1
changes/7765-queries-schedules-schema-updates
Normal file
1
changes/7765-queries-schedules-schema-updates
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Updated 'queries' table schema to allow storing scheduling information and configuration in the 'queries' table.
|
||||
|
|
@ -779,12 +779,6 @@ func newCleanupsAndAggregationSchedule(
|
|||
return ds.UpdateQueryAggregatedStats(ctx)
|
||||
},
|
||||
),
|
||||
schedule.WithJob(
|
||||
"scheduled_query_aggregated_stats",
|
||||
func(ctx context.Context) error {
|
||||
return ds.UpdateScheduledQueryAggregatedStats(ctx)
|
||||
},
|
||||
),
|
||||
schedule.WithJob(
|
||||
"aggregated_munki_and_mdm",
|
||||
func(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/cobra"
|
||||
_ "go.elastic.co/apm/module/apmsql"
|
||||
_ "go.elastic.co/apm/module/apmsql/mysql"
|
||||
_ "go.elastic.co/apm/module/apmsql/v2"
|
||||
_ "go.elastic.co/apm/module/apmsql/v2/mysql"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
|
|
|
|||
|
|
@ -1122,7 +1122,7 @@ spec:
|
|||
|
||||
// Apply queries.
|
||||
var appliedQueries []*fleet.Query
|
||||
ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error {
|
||||
|
|
@ -1223,7 +1223,7 @@ func TestApplyQueries(t *testing.T) {
|
|||
_, ds := runServerWithMockedDS(t)
|
||||
|
||||
var appliedQueries []*fleet.Query
|
||||
ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
ds.ApplyQueriesFunc = func(ctx context.Context, authorID uint, queries []*fleet.Query) error {
|
||||
|
|
|
|||
|
|
@ -64,5 +64,5 @@ func TestConvertFileStdout(t *testing.T) {
|
|||
os.Stdout = oldStdout
|
||||
w.Close()
|
||||
out, _ := ioutil.ReadAll(r)
|
||||
require.Equal(t, expected, out)
|
||||
require.Equal(t, string(expected), string(out))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,11 +78,11 @@ func TestDeleteQuery(t *testing.T) {
|
|||
_, ds := runServerWithMockedDS(t)
|
||||
|
||||
var deletedQuery string
|
||||
ds.DeleteQueryFunc = func(ctx context.Context, name string) error {
|
||||
ds.DeleteQueryFunc = func(ctx context.Context, teamID *uint, name string) error {
|
||||
deletedQuery = name
|
||||
return nil
|
||||
}
|
||||
ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
if name != "query1" {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ func printLabel(c *cli.Context, label *fleet.LabelSpec) error {
|
|||
return printSpec(c, spec)
|
||||
}
|
||||
|
||||
func printQuery(c *cli.Context, query *fleet.QuerySpec) error {
|
||||
func printQuerySpec(c *cli.Context, query *fleet.QuerySpec) error {
|
||||
spec := specGeneric{
|
||||
Kind: fleet.QueryKind,
|
||||
Version: fleet.ApiVersion,
|
||||
|
|
@ -298,12 +298,74 @@ func getCommand() *cli.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func queryToTableRow(query fleet.Query, teamName string) []string {
|
||||
platform := "all"
|
||||
if query.Platform != "" {
|
||||
platform = query.Platform
|
||||
}
|
||||
|
||||
minOsqueryVersion := "all"
|
||||
if query.MinOsqueryVersion != "" {
|
||||
minOsqueryVersion = query.MinOsqueryVersion
|
||||
}
|
||||
|
||||
scheduleInfo := fmt.Sprintf("interval: %d\nplatform: %s\nmin_osquery_version: %s\nautomations_enabled: %t\nlogging: %s",
|
||||
query.Interval,
|
||||
platform,
|
||||
minOsqueryVersion,
|
||||
query.AutomationsEnabled,
|
||||
query.Logging,
|
||||
)
|
||||
|
||||
teamNameOut := teamName
|
||||
if teamName == "" {
|
||||
teamNameOut = "All teams"
|
||||
}
|
||||
|
||||
return []string{
|
||||
query.Name,
|
||||
query.Description,
|
||||
query.Query,
|
||||
teamNameOut,
|
||||
scheduleInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func printInheritedQueriesMsg(client *service.Client, teamID *uint) error {
|
||||
if teamID != nil {
|
||||
globalQueries, err := client.GetQueries(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not list global queries: %w", err)
|
||||
}
|
||||
|
||||
if len(globalQueries) > 0 {
|
||||
fmt.Printf("Not showing %d inherited queries. To see global queries, run this command without the `--team` flag.\n", len(globalQueries))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNoQueriesFoundMsg(teamID *uint) {
|
||||
if teamID != nil {
|
||||
fmt.Println("No team queries found.")
|
||||
return
|
||||
}
|
||||
fmt.Println("No global queries found.")
|
||||
fmt.Println("To see team queries, run this command with the --team flag.")
|
||||
}
|
||||
|
||||
func getQueriesCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "queries",
|
||||
Aliases: []string{"query", "q"},
|
||||
Usage: "List information about one or more queries",
|
||||
Flags: []cli.Flag{
|
||||
&cli.UintFlag{
|
||||
Name: teamFlagName,
|
||||
Usage: "filter queries by team_id (0 means global)",
|
||||
},
|
||||
jsonFlag(),
|
||||
yamlFlag(),
|
||||
configFlag(),
|
||||
|
|
@ -318,9 +380,27 @@ func getQueriesCommand() *cli.Command {
|
|||
|
||||
name := c.Args().First()
|
||||
|
||||
// if name wasn't provided, list all queries
|
||||
var teamID *uint
|
||||
var teamName string
|
||||
|
||||
if tid := c.Uint(teamFlagName); tid != 0 {
|
||||
teamID = &tid
|
||||
team, err := client.GetTeam(*teamID)
|
||||
if err != nil {
|
||||
var notFoundErr service.NotFoundErr
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
// Do not error out, just inform the user and 'gracefully' exit.
|
||||
fmt.Println("Team not found.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("get team: %w", err)
|
||||
}
|
||||
teamName = team.Name
|
||||
}
|
||||
|
||||
// if name wasn't provided, list either all global queries or all team queries...
|
||||
if name == "" {
|
||||
queries, err := client.GetQueries()
|
||||
queries, err := client.GetQueries(teamID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not list queries: %w", err)
|
||||
}
|
||||
|
|
@ -350,44 +430,54 @@ func getQueriesCommand() *cli.Command {
|
|||
}
|
||||
|
||||
if len(queries) == 0 {
|
||||
fmt.Println("No queries found")
|
||||
printNoQueriesFoundMsg(teamID)
|
||||
if err := printInheritedQueriesMsg(client, teamID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) {
|
||||
for _, query := range queries {
|
||||
if err := printQuery(c, &fleet.QuerySpec{
|
||||
if err := printQuerySpec(c, &fleet.QuerySpec{
|
||||
Name: query.Name,
|
||||
Description: query.Description,
|
||||
Query: query.Query,
|
||||
|
||||
TeamName: teamName,
|
||||
Interval: query.Interval,
|
||||
ObserverCanRun: query.ObserverCanRun,
|
||||
Platform: query.Platform,
|
||||
MinOsqueryVersion: query.MinOsqueryVersion,
|
||||
AutomationsEnabled: query.AutomationsEnabled,
|
||||
Logging: query.Logging,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("unable to print query: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default to printing as a table
|
||||
data := [][]string{}
|
||||
rows := [][]string{}
|
||||
|
||||
columns := []string{"name", "description", "query", "team", "schedule"}
|
||||
for _, query := range queries {
|
||||
data = append(data, []string{
|
||||
query.Name,
|
||||
query.Description,
|
||||
query.Query,
|
||||
})
|
||||
rows = append(rows, queryToTableRow(query, teamName))
|
||||
}
|
||||
|
||||
columns := []string{"name", "description", "query"}
|
||||
printTable(c, columns, data)
|
||||
printQueryTable(c, columns, rows)
|
||||
if err := printInheritedQueriesMsg(client, teamID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
query, err := client.GetQuery(name)
|
||||
query, err := client.GetQuerySpec(teamID, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := printQuery(c, query); err != nil {
|
||||
if err := printQuerySpec(c, query); err != nil {
|
||||
return fmt.Errorf("unable to print query: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -426,7 +516,7 @@ func getPacksCommand() *cli.Command {
|
|||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: withQueriesFlagName,
|
||||
Usage: "Output queries included in pack(s) too",
|
||||
Usage: "Output queries included in pack(s) too, when used alongside --yaml or --json",
|
||||
},
|
||||
jsonFlag(),
|
||||
yamlFlag(),
|
||||
|
|
@ -457,7 +547,8 @@ func getPacksCommand() *cli.Command {
|
|||
return nil
|
||||
}
|
||||
|
||||
queries, err := client.GetQueries()
|
||||
// Get global queries (teamID==nil), because 2017 packs reference global queries.
|
||||
queries, err := client.GetQueries(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not list queries: %w", err)
|
||||
}
|
||||
|
|
@ -469,7 +560,7 @@ func getPacksCommand() *cli.Command {
|
|||
continue
|
||||
}
|
||||
|
||||
if err := printQuery(c, &fleet.QuerySpec{
|
||||
if err := printQuerySpec(c, &fleet.QuerySpec{
|
||||
Name: query.Name,
|
||||
Description: query.Description,
|
||||
Query: query.Query,
|
||||
|
|
@ -493,10 +584,8 @@ func getPacksCommand() *cli.Command {
|
|||
if err := printPack(c, pack); err != nil {
|
||||
return fmt.Errorf("unable to print pack: %w", err)
|
||||
}
|
||||
|
||||
addQueries(pack)
|
||||
}
|
||||
|
||||
return printQueries()
|
||||
}
|
||||
|
||||
|
|
@ -994,6 +1083,14 @@ func getUserRolesCommand() *cli.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func printQueryTable(c *cli.Context, columns []string, data [][]string) {
|
||||
table := defaultTable(c.App.Writer)
|
||||
table.SetHeader(columns)
|
||||
table.SetReflowDuringAutoWrap(false)
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func printTable(c *cli.Context, columns []string, data [][]string) {
|
||||
table := defaultTable(c.App.Writer)
|
||||
table.SetHeader(columns)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
|
@ -972,92 +973,313 @@ spec:
|
|||
}
|
||||
|
||||
func TestGetQueries(t *testing.T) {
|
||||
_, ds := runServerWithMockedDS(t)
|
||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{
|
||||
Tier: fleet.TierPremium,
|
||||
Expiration: time.Now().Add(24 * time.Hour),
|
||||
},
|
||||
})
|
||||
|
||||
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
|
||||
return []*fleet.Query{
|
||||
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
|
||||
return []*fleet.TeamSummary{
|
||||
{
|
||||
ID: 33,
|
||||
Name: "query1",
|
||||
Description: "some desc",
|
||||
Query: "select 1;",
|
||||
Saved: false,
|
||||
ObserverCanRun: false,
|
||||
},
|
||||
{
|
||||
ID: 12,
|
||||
Name: "query2",
|
||||
Description: "some desc 2",
|
||||
Query: "select 2;",
|
||||
Saved: true,
|
||||
ObserverCanRun: false,
|
||||
ID: 1,
|
||||
Name: "Foobar",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
||||
if tid == 1 {
|
||||
return &fleet.Team{
|
||||
ID: tid,
|
||||
Name: "Foobar",
|
||||
}, nil
|
||||
}
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) {
|
||||
if opt.TeamID == nil {
|
||||
return []*fleet.Query{
|
||||
{
|
||||
ID: 33,
|
||||
Name: "query1",
|
||||
Description: "some desc",
|
||||
Query: "select 1;",
|
||||
Saved: true, // ListQueries always returns the saved ones.
|
||||
ObserverCanRun: false,
|
||||
},
|
||||
{
|
||||
ID: 12,
|
||||
Name: "query2",
|
||||
Description: "some desc 2",
|
||||
Query: "select 2;",
|
||||
Saved: true, // ListQueries always returns the saved ones.
|
||||
ObserverCanRun: false,
|
||||
},
|
||||
{
|
||||
ID: 14,
|
||||
Name: "query4",
|
||||
Description: "some desc 4",
|
||||
Query: "select 4;",
|
||||
Interval: 60,
|
||||
AutomationsEnabled: true,
|
||||
MinOsqueryVersion: "5.3.0",
|
||||
Platform: "darwin,windows",
|
||||
Logging: "differential_ignore_removals",
|
||||
Saved: true, // ListQueries always returns the saved ones.
|
||||
ObserverCanRun: true,
|
||||
},
|
||||
}, nil
|
||||
} else if *opt.TeamID == 1 {
|
||||
return []*fleet.Query{
|
||||
{
|
||||
ID: 13,
|
||||
Name: "query3",
|
||||
Description: "some desc 3",
|
||||
Query: "select 3;",
|
||||
Interval: 3600,
|
||||
AutomationsEnabled: false,
|
||||
MinOsqueryVersion: "5.4.0",
|
||||
Platform: "darwin",
|
||||
Logging: "snapshot",
|
||||
Saved: true, // ListQueries always returns the saved ones.
|
||||
TeamID: ptr.Uint(1),
|
||||
ObserverCanRun: true,
|
||||
},
|
||||
}, nil
|
||||
} else if *opt.TeamID == 2 {
|
||||
return []*fleet.Query{}, nil
|
||||
}
|
||||
return nil, errors.New("invalid team ID")
|
||||
}
|
||||
|
||||
expected := `+--------+-------------+-----------+
|
||||
| NAME | DESCRIPTION | QUERY |
|
||||
+--------+-------------+-----------+
|
||||
| query1 | some desc | select 1; |
|
||||
+--------+-------------+-----------+
|
||||
| query2 | some desc 2 | select 2; |
|
||||
+--------+-------------+-----------+
|
||||
expectedGlobal := `+--------+-------------+-----------+-----------+--------------------------------+
|
||||
| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE |
|
||||
+--------+-------------+-----------+-----------+--------------------------------+
|
||||
| query1 | some desc | select 1; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+--------------------------------+
|
||||
| query2 | some desc 2 | select 2; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+--------------------------------+
|
||||
| query4 | some desc 4 | select 4; | All teams | interval: 60 |
|
||||
| | | | | |
|
||||
| | | | | platform: darwin,windows |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: 5.3.0 |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: true |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
| | | | | differential_ignore_removals |
|
||||
+--------+-------------+-----------+-----------+--------------------------------+
|
||||
`
|
||||
expectedYaml := `---
|
||||
|
||||
expectedYAMLGlobal := `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: query1
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select 1;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 2
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: query2
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select 2;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: true
|
||||
description: some desc 4
|
||||
interval: 60
|
||||
logging: differential_ignore_removals
|
||||
min_osquery_version: 5.3.0
|
||||
name: query4
|
||||
observer_can_run: true
|
||||
platform: darwin,windows
|
||||
query: select 4;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;"}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;"}}
|
||||
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"}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"}))
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "queries", "--yaml"}))
|
||||
assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "queries", "--json"}))
|
||||
expectedTeam := `+--------+-------------+-----------+--------+----------------------------+
|
||||
| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE |
|
||||
+--------+-------------+-----------+--------+----------------------------+
|
||||
| query3 | some desc 3 | select 3; | Foobar | interval: 3600 |
|
||||
| | | | | |
|
||||
| | | | | platform: darwin |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: 5.4.0 |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: snapshot |
|
||||
+--------+-------------+-----------+--------+----------------------------+
|
||||
`
|
||||
|
||||
expectedYAMLTeam := `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 3
|
||||
interval: 3600
|
||||
logging: snapshot
|
||||
min_osquery_version: 5.4.0
|
||||
name: query3
|
||||
observer_can_run: true
|
||||
platform: darwin
|
||||
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"}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expectedGlobal, runAppForTest(t, []string{"get", "queries"}))
|
||||
assert.Equal(t, expectedYAMLGlobal, runAppForTest(t, []string{"get", "queries", "--yaml"}))
|
||||
assert.Equal(t, expectedJSONGlobal, runAppForTest(t, []string{"get", "queries", "--json"}))
|
||||
|
||||
assert.Equal(t, expectedTeam, runAppForTest(t, []string{"get", "queries", "--team", "1"}))
|
||||
assert.Equal(t, expectedYAMLTeam, runAppForTest(t, []string{"get", "queries", "--yaml", "--team", "1"}))
|
||||
assert.Equal(t, expectedJSONTeam, runAppForTest(t, []string{"get", "queries", "--json", "--team", "1"}))
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"get", "queries", "--team", "2"}))
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"get", "queries", "--yaml", "--team", "2"}))
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"get", "queries", "--json", "--team", "2"}))
|
||||
}
|
||||
|
||||
func TestGetQuery(t *testing.T) {
|
||||
_, ds := runServerWithMockedDS(t)
|
||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{
|
||||
Tier: fleet.TierPremium,
|
||||
Expiration: time.Now().Add(24 * time.Hour),
|
||||
},
|
||||
})
|
||||
|
||||
ds.QueryByNameFunc = func(ctx context.Context, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
if name != "query1" {
|
||||
return nil, nil
|
||||
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
||||
if tid == 1 {
|
||||
return &fleet.Team{
|
||||
ID: tid,
|
||||
Name: "Foobar",
|
||||
}, nil
|
||||
}
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string, opts ...fleet.OptionalArg) (*fleet.Query, error) {
|
||||
if teamID == nil {
|
||||
if name != "globalQuery1" {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
return &fleet.Query{
|
||||
ID: 33,
|
||||
Name: "globalQuery1",
|
||||
Description: "some desc",
|
||||
Query: "select 1;",
|
||||
Saved: true,
|
||||
ObserverCanRun: false,
|
||||
}, nil
|
||||
} else if *teamID == 1 {
|
||||
if name != "teamQuery1" {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
return &fleet.Query{
|
||||
ID: 34,
|
||||
Name: "teamQuery1",
|
||||
Description: "some team desc",
|
||||
Query: "select 2;",
|
||||
Saved: true,
|
||||
ObserverCanRun: true,
|
||||
TeamID: teamID,
|
||||
|
||||
Interval: 3600,
|
||||
AutomationsEnabled: true,
|
||||
MinOsqueryVersion: "5.2.0",
|
||||
Platform: "linux",
|
||||
Logging: "differential",
|
||||
}, nil
|
||||
} else {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
return &fleet.Query{
|
||||
ID: 33,
|
||||
Name: "query1",
|
||||
Description: "some desc",
|
||||
Query: "select 1;",
|
||||
Saved: false,
|
||||
ObserverCanRun: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
expectedYaml := `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc
|
||||
name: query1
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: globalQuery1
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select 1;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;"}}
|
||||
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":""}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "query1"}))
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--yaml", "query1"}))
|
||||
assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "query", "--json", "query1"}))
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "globalQuery1"}))
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--yaml", "globalQuery1"}))
|
||||
assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "query", "--json", "globalQuery1"}))
|
||||
|
||||
expectedYaml = `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: true
|
||||
description: some team desc
|
||||
interval: 3600
|
||||
logging: differential
|
||||
min_osquery_version: 5.2.0
|
||||
name: teamQuery1
|
||||
observer_can_run: true
|
||||
platform: linux
|
||||
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"}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--team", "1", "teamQuery1"}))
|
||||
assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "query", "--yaml", "--team", "1", "teamQuery1"}))
|
||||
assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "query", "--json", "--team", "1", "teamQuery1"}))
|
||||
}
|
||||
|
||||
// TestGetQueriesAsObservers tests that when observers run `fleectl get queries` they
|
||||
|
|
@ -1157,21 +1379,36 @@ func TestGetQueriesAsObserver(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
setCurrentUserSession(tc.user)
|
||||
|
||||
expected := `+--------+-------------+-----------+
|
||||
| NAME | DESCRIPTION | QUERY |
|
||||
+--------+-------------+-----------+
|
||||
| query2 | some desc 2 | select 2; |
|
||||
+--------+-------------+-----------+
|
||||
expected := `+--------+-------------+-----------+-----------+----------------------------+
|
||||
| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
| query2 | some desc 2 | select 2; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
`
|
||||
expectedYaml := `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 2
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: query2
|
||||
observer_can_run: true
|
||||
platform: ""
|
||||
query: select 2;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;"}}
|
||||
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":""}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"}))
|
||||
|
|
@ -1199,41 +1436,86 @@ spec:
|
|||
},
|
||||
})
|
||||
|
||||
expected := `+--------+-------------+-----------+
|
||||
| NAME | DESCRIPTION | QUERY |
|
||||
+--------+-------------+-----------+
|
||||
| query1 | some desc | select 1; |
|
||||
+--------+-------------+-----------+
|
||||
| query2 | some desc 2 | select 2; |
|
||||
+--------+-------------+-----------+
|
||||
| query3 | some desc 3 | select 3; |
|
||||
+--------+-------------+-----------+
|
||||
expected := `+--------+-------------+-----------+-----------+----------------------------+
|
||||
| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
| query1 | some desc | select 1; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
| query2 | some desc 2 | select 2; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
| query3 | some desc 3 | select 3; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
`
|
||||
expectedYaml := `---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: query1
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select 1;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 2
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: query2
|
||||
observer_can_run: true
|
||||
platform: ""
|
||||
query: select 2;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: some desc 3
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: query3
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select 3;
|
||||
team: ""
|
||||
`
|
||||
expectedJson := `{"kind":"query","apiVersion":"v1","spec":{"name":"query1","description":"some desc","query":"select 1;"}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query2","description":"some desc 2","query":"select 2;"}}
|
||||
{"kind":"query","apiVersion":"v1","spec":{"name":"query3","description":"some desc 3","query":"select 3;"}}
|
||||
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":""}}
|
||||
`
|
||||
|
||||
assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"}))
|
||||
|
|
@ -1288,13 +1570,29 @@ spec:
|
|||
},
|
||||
}, nil
|
||||
}
|
||||
expected = `+--------+-------------+-----------+
|
||||
| NAME | DESCRIPTION | QUERY |
|
||||
+--------+-------------+-----------+
|
||||
| query1 | some desc | select 1; |
|
||||
+--------+-------------+-----------+
|
||||
| query2 | some desc 2 | select 2; |
|
||||
+--------+-------------+-----------+
|
||||
expected = `+--------+-------------+-----------+-----------+----------------------------+
|
||||
| NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
| query1 | some desc | select 1; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
| query2 | some desc 2 | select 2; | All teams | interval: 0 |
|
||||
| | | | | |
|
||||
| | | | | platform: all |
|
||||
| | | | | |
|
||||
| | | | | min_osquery_version: all |
|
||||
| | | | | |
|
||||
| | | | | automations_enabled: false |
|
||||
| | | | | |
|
||||
| | | | | logging: |
|
||||
+--------+-------------+-----------+-----------+----------------------------+
|
||||
`
|
||||
assert.Equal(t, expected, runAppForTest(t, []string{"get", "queries"}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ func queryCommand() *cli.Command {
|
|||
Destination: &flTimeout,
|
||||
Usage: "How long to run query before exiting (10s, 1h, etc.)",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: teamFlagName,
|
||||
Usage: "ID of the team where the named query belongs to (0 means global)",
|
||||
},
|
||||
configFlag(),
|
||||
contextFlag(),
|
||||
debugFlag(),
|
||||
|
|
@ -94,7 +98,11 @@ func queryCommand() *cli.Command {
|
|||
}
|
||||
|
||||
if flQueryName != "" {
|
||||
q, err := fleet.GetQuery(flQueryName)
|
||||
var teamID *uint
|
||||
if tid := c.Uint(teamFlagName); tid != 0 {
|
||||
teamID = &tid
|
||||
}
|
||||
q, err := fleet.GetQuerySpec(teamID, flQueryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query '%s' not found", flQueryName)
|
||||
}
|
||||
|
|
|
|||
35
cmd/fleetctl/testdata/convert_output.yml
vendored
35
cmd/fleetctl/testdata/convert_output.yml
vendored
|
|
@ -44,35 +44,70 @@ spec:
|
|||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the list of application scheme/protocol-based IPC handlers.
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: app_schemes
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select * from app_schemes;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the current disk encryption status for the target system.
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: disk_encryption
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select * from disk_encryption;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves the current filters and chains per filter in the target system.
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: iptables
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select * from iptables;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: Retrieves all the daemons that will run in the start of the target
|
||||
OSX system.
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: launchd
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select * from launchd;
|
||||
team: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
automations_enabled: false
|
||||
description: Lists the application bundle that owns a sandbox label.
|
||||
interval: 0
|
||||
logging: ""
|
||||
min_osquery_version: ""
|
||||
name: sandboxes
|
||||
observer_can_run: false
|
||||
platform: ""
|
||||
query: select * from sandboxes;
|
||||
team: ""
|
||||
|
|
|
|||
39
frontend/__mocks__/scheduleableQueryMock.ts
Normal file
39
frontend/__mocks__/scheduleableQueryMock.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// "SchedulableQuery" to be used in developing frontend for #7765
|
||||
|
||||
import { ISchedulableQuery } from "interfaces/schedulable_query";
|
||||
|
||||
const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = {
|
||||
created_at: "2022-11-03T17:22:14Z",
|
||||
updated_at: "2022-11-03T17:22:14Z",
|
||||
id: 1,
|
||||
name: "Test Query",
|
||||
description: "A test query",
|
||||
query: "SELECT * FROM users",
|
||||
team_id: null,
|
||||
interval: 43200, // Every 12 hours
|
||||
platform: "darwin,windows,linux",
|
||||
min_osquery_version: "",
|
||||
automations_enabled: true,
|
||||
logging: "snapshot",
|
||||
saved: true,
|
||||
author_id: 1,
|
||||
author_name: "Test User",
|
||||
author_email: "test@example.com",
|
||||
observer_can_run: false,
|
||||
packs: [],
|
||||
stats: {
|
||||
system_time_p50: 28.1053,
|
||||
system_time_p95: 397.6667,
|
||||
user_time_p50: 29.9412,
|
||||
user_time_p95: 251.4615,
|
||||
total_executions: 5746,
|
||||
},
|
||||
};
|
||||
|
||||
const createMockSchedulableQuery = (
|
||||
overrides?: Partial<ISchedulableQuery>
|
||||
): ISchedulableQuery => {
|
||||
return { ...DEFAULT_SCHEDULABLE_QUERY_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
export default createMockSchedulableQuery;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import LogDestinationIndicator from "./LogDestinationIndicator";
|
||||
|
||||
const meta: Meta<typeof LogDestinationIndicator> = {
|
||||
title: "Components/LogDestinationIndicator",
|
||||
component: LogDestinationIndicator,
|
||||
args: {
|
||||
logDestination: "filesystem",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof LogDestinationIndicator>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
interface ILogDestinationIndicatorProps {
|
||||
logDestination: string;
|
||||
}
|
||||
|
||||
const generateClassTag = (rawValue: string): string => {
|
||||
if (rawValue === DEFAULT_EMPTY_CELL_VALUE) {
|
||||
return "indeterminate";
|
||||
}
|
||||
return rawValue.replace(" ", "-").toLowerCase();
|
||||
};
|
||||
|
||||
const LogDestinationIndicator = ({
|
||||
logDestination,
|
||||
}: ILogDestinationIndicatorProps): JSX.Element => {
|
||||
const classTag = generateClassTag(logDestination);
|
||||
const statusClassName = classnames(
|
||||
"log-destination-indicator",
|
||||
`log-destination-indicator--${classTag}`,
|
||||
`log-destination--${classTag}`
|
||||
);
|
||||
const readableLogDestination = () => {
|
||||
switch (logDestination) {
|
||||
case "filesystem":
|
||||
return "Filesystem";
|
||||
case "firehose":
|
||||
return "Amazon Kinesis Data Firehose";
|
||||
case "kinesis":
|
||||
return "Amazon Kinesis Data Streams";
|
||||
case "lambda":
|
||||
return "AWS Lambda";
|
||||
case "pubsub":
|
||||
return "Google Cloud Pub/Sub";
|
||||
case "kafta":
|
||||
return "Apache Kafka";
|
||||
case "stdout":
|
||||
return "Standard output (stdout)";
|
||||
case "":
|
||||
return "Not configured";
|
||||
default:
|
||||
return logDestination;
|
||||
}
|
||||
};
|
||||
|
||||
const tooltipText = () => {
|
||||
switch (logDestination) {
|
||||
case "filesystem":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
/var/log/osquery/osqueryd.snapshots.log <br />
|
||||
in each host's filesystem.`;
|
||||
case "firehose":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
Amazon Kinesis Data Firehose.`;
|
||||
case "kinesis":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
Amazon Kinesis Data Streams.`;
|
||||
case "lambda":
|
||||
return `
|
||||
Each time a query runs, the data <br />is sent to AWS Lambda.
|
||||
`;
|
||||
case "pubsub":
|
||||
return `Each time a query runs, the data is <br />sent to Google Cloud Pub/Sub.`;
|
||||
case "kafta":
|
||||
return `Each time a query runs, the data <br />is sent to Apache Kafka.`;
|
||||
case "stdout":
|
||||
return `Each time a query runs, the data is sent to <br />
|
||||
standard output (stdout) on the Fleet server.`;
|
||||
case "":
|
||||
return "Please configure a log destination.";
|
||||
default:
|
||||
return "No additional information is available about this log destination.";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipWrapper tipContent={tooltipText()} className={statusClassName}>
|
||||
{readableLogDestination()}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogDestinationIndicator;
|
||||
1
frontend/components/LogDestinationIndicator/index.ts
Normal file
1
frontend/components/LogDestinationIndicator/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./LogDestinationIndicator";
|
||||
|
|
@ -11,6 +11,7 @@ export interface IModalProps {
|
|||
children: JSX.Element;
|
||||
onExit: () => void;
|
||||
onEnter?: () => void;
|
||||
/** default 650px, large 800px, xlarge 850px, auto auto-width */
|
||||
width?: ModalWidth;
|
||||
className?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react";
|
||||
|
||||
import { IOsqueryPlatform } from "interfaces/platform";
|
||||
import { OsqueryPlatform } from "interfaces/platform";
|
||||
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
|
||||
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
interface IPlatformCompatibilityProps {
|
||||
compatiblePlatforms: IOsqueryPlatform[] | null;
|
||||
compatiblePlatforms: OsqueryPlatform[] | null;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
|
|
@ -18,13 +18,13 @@ const DISPLAY_ORDER = [
|
|||
"Windows",
|
||||
"Linux",
|
||||
"ChromeOS",
|
||||
] as IOsqueryPlatform[];
|
||||
] as OsqueryPlatform[];
|
||||
|
||||
const ERROR_NO_COMPATIBLE_TABLES = Error("no tables in query");
|
||||
|
||||
const formatPlatformsForDisplay = (
|
||||
compatiblePlatforms: IOsqueryPlatform[]
|
||||
): IOsqueryPlatform[] => {
|
||||
compatiblePlatforms: OsqueryPlatform[]
|
||||
): OsqueryPlatform[] => {
|
||||
return compatiblePlatforms.map((str) => PLATFORM_DISPLAY_NAMES[str] || str);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
font-size: $x-small;
|
||||
align-items: center;
|
||||
padding-top: $pad-medium;
|
||||
padding-bottom: $pad-large;
|
||||
|
||||
b,
|
||||
svg,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import QueryFrequencyIndicator from "./QueryFrequencyIndicator";
|
||||
|
||||
const meta: Meta<typeof QueryFrequencyIndicator> = {
|
||||
title: "Components/QueryFrequencyIndicator",
|
||||
component: QueryFrequencyIndicator,
|
||||
args: {
|
||||
frequency: 300,
|
||||
checked: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof QueryFrequencyIndicator>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
interface IStatusIndicatorProps {
|
||||
frequency: number;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
const generateClassTag = (rawValue: string): string => {
|
||||
if (rawValue === DEFAULT_EMPTY_CELL_VALUE) {
|
||||
return "indeterminate";
|
||||
}
|
||||
return rawValue.replace(" ", "-").toLowerCase();
|
||||
};
|
||||
|
||||
const QueryFrequencyIndicator = ({
|
||||
frequency,
|
||||
checked,
|
||||
}: IStatusIndicatorProps): JSX.Element => {
|
||||
const classTag = generateClassTag(frequency.toString());
|
||||
const frequencyClassName = classnames(
|
||||
"query-frequency-indicator",
|
||||
`query-frequency-indicator--${classTag}`,
|
||||
`frequency--${classTag}`
|
||||
);
|
||||
const readableQueryFrequency = () => {
|
||||
switch (frequency) {
|
||||
case 0:
|
||||
return "Never";
|
||||
case 300:
|
||||
case 600:
|
||||
case 900:
|
||||
case 1800: // 5, 10, 15, 30 minutes
|
||||
return `${(frequency / 60).toString()} minutes`;
|
||||
case 3600:
|
||||
return "Hourly";
|
||||
case 21600:
|
||||
case 43200: // 6, 12 hours
|
||||
return `${(frequency / 3600).toString()} hours`;
|
||||
case 86400:
|
||||
return "Daily";
|
||||
case 604800:
|
||||
return "Weekly";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
const frequencyIcon = () => {
|
||||
if (frequency === 0) {
|
||||
return checked ? (
|
||||
<Icon size="small" name="warning" />
|
||||
) : (
|
||||
<Icon size="small" name="clock" color="ui-fleet-black-33" />
|
||||
);
|
||||
}
|
||||
return <Icon size="small" name="clock" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${frequencyClassName}
|
||||
${frequency === 0 && !checked && "grey"}`}
|
||||
>
|
||||
{frequencyIcon()}
|
||||
{readableQueryFrequency()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryFrequencyIndicator;
|
||||
14
frontend/components/QueryFrequencyIndicator/_styles.scss
Normal file
14
frontend/components/QueryFrequencyIndicator/_styles.scss
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.query-frequency-indicator {
|
||||
width: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
|
||||
.icon {
|
||||
padding-right: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: $ui-fleet-black-33;
|
||||
}
|
||||
1
frontend/components/QueryFrequencyIndicator/index.ts
Normal file
1
frontend/components/QueryFrequencyIndicator/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./QueryFrequencyIndicator";
|
||||
|
|
@ -8,7 +8,6 @@ const meta: Meta<typeof StatusIndicator> = {
|
|||
args: {
|
||||
value: "100",
|
||||
tooltip: {
|
||||
id: 1,
|
||||
tooltipText: "Tooltip text",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,58 +2,72 @@ import React from "react";
|
|||
import classnames from "classnames";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import { uniqueId } from "lodash";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
interface IStatusIndicatorProps {
|
||||
value: string;
|
||||
tooltip?: {
|
||||
id: number;
|
||||
tooltipText: string;
|
||||
tooltipText: string | JSX.Element;
|
||||
position?: "top" | "bottom";
|
||||
};
|
||||
customIndicatorType?: string;
|
||||
}
|
||||
|
||||
const generateClassTag = (rawValue: string): string => {
|
||||
const generateIndicatorStateClassTag = (
|
||||
rawValue: string,
|
||||
customIndicatorType?: string
|
||||
): string => {
|
||||
if (rawValue === DEFAULT_EMPTY_CELL_VALUE) {
|
||||
return "indeterminate";
|
||||
}
|
||||
return rawValue.replace(" ", "-").toLowerCase();
|
||||
const prefix = customIndicatorType ? `${customIndicatorType}-` : "";
|
||||
return `${prefix}${rawValue.replace(" ", "-").toLowerCase()}`;
|
||||
};
|
||||
|
||||
const StatusIndicator = ({
|
||||
value,
|
||||
tooltip,
|
||||
customIndicatorType,
|
||||
}: IStatusIndicatorProps): JSX.Element => {
|
||||
const classTag = generateClassTag(value);
|
||||
const statusClassName = classnames(
|
||||
const indicatorStateClassTag = generateIndicatorStateClassTag(
|
||||
value,
|
||||
customIndicatorType
|
||||
);
|
||||
const indicatorClassNames = classnames(
|
||||
"status-indicator",
|
||||
`status-indicator--${classTag}`,
|
||||
`status--${classTag}`
|
||||
`status-indicator--${indicatorStateClassTag}`,
|
||||
`status--${indicatorStateClassTag}`
|
||||
);
|
||||
const indicatorContent = tooltip ? (
|
||||
<>
|
||||
<span
|
||||
className="host-status tooltip tooltip__tooltip-icon"
|
||||
data-tip
|
||||
data-for={`status-${tooltip.id}`}
|
||||
data-tip-disable={false}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className="status-tooltip"
|
||||
place={tooltip?.position ? tooltip.position : "top"}
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`status-${tooltip.id}`}
|
||||
backgroundColor="#3e4771"
|
||||
>
|
||||
{tooltip.tooltipText}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
) : (
|
||||
<>{value}</>
|
||||
);
|
||||
return <span className={statusClassName}>{indicatorContent}</span>;
|
||||
let indicatorContent;
|
||||
if (tooltip) {
|
||||
const tooltipId = uniqueId();
|
||||
indicatorContent = (
|
||||
<>
|
||||
<span
|
||||
className="host-status tooltip tooltip__tooltip-icon"
|
||||
data-tip
|
||||
data-for={`status-${tooltipId}`}
|
||||
data-tip-disable={false}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className="status-tooltip"
|
||||
place={tooltip?.position ? tooltip.position : "top"}
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`status-${tooltipId}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
{tooltip.tooltipText}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
indicatorContent = <>{value}</>;
|
||||
}
|
||||
return <span className={indicatorClassNames}>{indicatorContent}</span>;
|
||||
};
|
||||
|
||||
export default StatusIndicator;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ const PERFORMANCE_IMPACT = { indicator: "Minimal", id: 3 };
|
|||
|
||||
describe("Pill cell", () => {
|
||||
it("renders pill text and tooltip on hover", async () => {
|
||||
const { user } = renderWithSetup(
|
||||
<PillCell value={PERFORMANCE_IMPACT} hostDetails />
|
||||
);
|
||||
const { user } = renderWithSetup(<PillCell value={PERFORMANCE_IMPACT} />);
|
||||
|
||||
await user.hover(screen.getByText("Minimal"));
|
||||
|
||||
|
|
|
|||
|
|
@ -75,9 +75,8 @@ const PillCell = ({
|
|||
case "Undetermined":
|
||||
return (
|
||||
<>
|
||||
To see performance <br /> impact, this query must <br /> run as a
|
||||
scheduled query <br /> on {hostDetails ? "this" : "at least one"}{" "}
|
||||
host.
|
||||
To see performance impact, this query must have run with{" "}
|
||||
<b>automations</b> on {hostDetails ? "this" : "at least one"} host.
|
||||
</>
|
||||
);
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const meta: Meta<typeof PlatformCell> = {
|
|||
title: "Components/Table/PlatformCell",
|
||||
component: PlatformCell,
|
||||
args: {
|
||||
value: ["darwin", "windows", "linux"],
|
||||
platforms: ["darwin", "windows", "linux"],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import React from "react";
|
||||
import { getByTestId, render, screen, within } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
import { SupportedPlatform } from "interfaces/platform";
|
||||
import PlatformCell from "./PlatformCell";
|
||||
|
||||
const PLATFORMS = ["windows", "darwin", "linux", "chrome"];
|
||||
const PLATFORMS: SupportedPlatform[] = ["windows", "darwin", "linux", "chrome"];
|
||||
|
||||
describe("Platform cell", () => {
|
||||
it("renders platform icons in correct order", () => {
|
||||
render(<PlatformCell value={PLATFORMS} />);
|
||||
render(<PlatformCell platforms={PLATFORMS} />);
|
||||
|
||||
const icons = screen.queryAllByTestId("icon");
|
||||
const appleIcon = screen.queryByTestId("apple-icon");
|
||||
|
|
@ -23,7 +24,7 @@ describe("Platform cell", () => {
|
|||
expect(icons[3].firstChild).toBe(chromeIcon);
|
||||
});
|
||||
it("renders empty state", () => {
|
||||
render(<PlatformCell value={[]} />);
|
||||
render(<PlatformCell platforms={[]} />);
|
||||
|
||||
const icons = screen.queryAllByTestId("icon");
|
||||
const emptyText = screen.queryByText(DEFAULT_EMPTY_CELL_VALUE);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import React from "react";
|
||||
import Icon from "components/Icon";
|
||||
import { SupportedPlatform } from "interfaces/platform";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
interface IPlatformCellProps {
|
||||
value: string[];
|
||||
platforms: SupportedPlatform[];
|
||||
}
|
||||
|
||||
const baseClass = "platform-cell";
|
||||
|
|
@ -14,7 +16,7 @@ const ICONS: Record<string, "darwin" | "windows" | "linux" | "chrome"> = {
|
|||
chrome: "chrome",
|
||||
};
|
||||
|
||||
const DISPLAY_ORDER = [
|
||||
const DISPLAY_ORDER: SupportedPlatform[] = [
|
||||
"darwin",
|
||||
"windows",
|
||||
"linux",
|
||||
|
|
@ -23,9 +25,7 @@ const DISPLAY_ORDER = [
|
|||
// "Invalid query",
|
||||
];
|
||||
|
||||
const PlatformCell = ({
|
||||
value: platforms,
|
||||
}: IPlatformCellProps): JSX.Element => {
|
||||
const PlatformCell = ({ platforms }: IPlatformCellProps): JSX.Element => {
|
||||
const orderedList = DISPLAY_ORDER.filter((platform) =>
|
||||
platforms.includes(platform)
|
||||
);
|
||||
|
|
@ -43,7 +43,9 @@ const PlatformCell = ({
|
|||
) : null;
|
||||
})
|
||||
) : (
|
||||
<span className={`${baseClass}__muted`}>---</span>
|
||||
<span className={`${baseClass}__muted`}>
|
||||
{DEFAULT_EMPTY_CELL_VALUE}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { uniqueId } from "lodash";
|
||||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
interface ITextCellProps {
|
||||
|
|
@ -6,6 +8,7 @@ interface ITextCellProps {
|
|||
formatter?: (val: any) => JSX.Element | string; // string, number, or null
|
||||
greyed?: boolean;
|
||||
classes?: string;
|
||||
emptyCellTooltipText?: JSX.Element | string;
|
||||
}
|
||||
|
||||
const TextCell = ({
|
||||
|
|
@ -13,6 +16,7 @@ const TextCell = ({
|
|||
formatter = (val) => val, // identity function if no formatter is provided
|
||||
greyed,
|
||||
classes = "w250",
|
||||
emptyCellTooltipText,
|
||||
}: ITextCellProps): JSX.Element => {
|
||||
let val = value;
|
||||
|
||||
|
|
@ -22,9 +26,32 @@ const TextCell = ({
|
|||
if (!val) {
|
||||
greyed = true;
|
||||
}
|
||||
|
||||
const renderEmptyCell = () => {
|
||||
if (emptyCellTooltipText) {
|
||||
const tooltipId = uniqueId();
|
||||
return (
|
||||
<>
|
||||
<span data-tip data-for={tooltipId}>
|
||||
{DEFAULT_EMPTY_CELL_VALUE}
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="top"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
id={tooltipId}
|
||||
>
|
||||
{emptyCellTooltipText}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return DEFAULT_EMPTY_CELL_VALUE;
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`text-cell ${classes} ${greyed && "grey-cell"}`}>
|
||||
{formatter(val) || DEFAULT_EMPTY_CELL_VALUE}
|
||||
{formatter(val) || renderEmptyCell()}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -216,10 +216,11 @@ $shadow-transition-width: 10px;
|
|||
.link-cell,
|
||||
.text-cell {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
.__react_component_tooltip {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
.w400 {
|
||||
max-width: calc(400px - 48px);
|
||||
|
|
@ -234,6 +235,9 @@ $shadow-transition-width: 10px;
|
|||
.grey-cell {
|
||||
color: $ui-fleet-black-50;
|
||||
font-style: italic;
|
||||
.__react_component_tooltip {
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const RevealButton = ({
|
|||
{caretPosition === "before" && (
|
||||
<Icon
|
||||
name="chevron"
|
||||
direction={isShowing ? "right" : "down"}
|
||||
direction={isShowing ? "down" : "right"}
|
||||
color="core-fleet-blue"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ class InputFieldWithIcon extends InputField {
|
|||
{ [`${baseClass}__icon--active`]: value }
|
||||
);
|
||||
|
||||
console.log("iconSvg", iconSvg);
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{this.props.label && this.renderHeading()}
|
||||
|
|
|
|||
39
frontend/components/icons/Clock.tsx
Normal file
39
frontend/components/icons/Clock.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
|
||||
import { COLORS, Colors } from "styles/var/colors";
|
||||
import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes";
|
||||
|
||||
interface IClockProps {
|
||||
color?: Colors;
|
||||
size?: IconSizes;
|
||||
}
|
||||
|
||||
const Clock = ({
|
||||
color = "ui-fleet-black-75",
|
||||
size = "small",
|
||||
}: IClockProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={ICON_SIZES[size]}
|
||||
height={ICON_SIZES[size]}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 13"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 11a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9Zm0 1.5a6 6 0 1 0 0-12 6 6 0 0 0 0 12Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6 3.125a.75.75 0 0 1 .75.75V5.75h1.125a.75.75 0 0 1 0 1.5H6a.75.75 0 0 1-.75-.75V3.875a.75.75 0 0 1 .75-.75Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clock;
|
||||
33
frontend/components/icons/Warning.tsx
Normal file
33
frontend/components/icons/Warning.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
|
||||
import { COLORS, Colors } from "styles/var/colors";
|
||||
import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes";
|
||||
|
||||
interface IWarningProps {
|
||||
color?: Colors;
|
||||
size?: IconSizes;
|
||||
}
|
||||
|
||||
const Warning = ({
|
||||
color = "status-warning",
|
||||
size = "small",
|
||||
}: IWarningProps) => {
|
||||
return (
|
||||
<svg
|
||||
width={ICON_SIZES[size]}
|
||||
height={ICON_SIZES[size]}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 13"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="m11.887 11.214.008-.008L6.645.92l-.007.009C6.51.67 6.277.5 6 .5c-.277 0-.503.171-.638.429L5.356.92.105 11.206l.008.008a.898.898 0 0 0-.113.429c0 .471.338.857.75.857h10.5c.412 0 .75-.386.75-.857 0-.163-.045-.3-.113-.429ZM6 4.25a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0V5A.75.75 0 0 1 6 4.25ZM6 11a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Warning;
|
||||
|
|
@ -51,6 +51,8 @@ import Pending from "./Pending";
|
|||
import PendingPartial from "./PendingPartial";
|
||||
import ErrorOutline from "./ErrorOutline";
|
||||
import Error from "./Error";
|
||||
import Warning from "./Warning";
|
||||
import Clock from "./Clock";
|
||||
|
||||
import Copy from "./Copy";
|
||||
import Eye from "./Eye";
|
||||
|
|
@ -110,6 +112,8 @@ export const ICON_MAP = {
|
|||
"pending-partial": PendingPartial,
|
||||
error: Error,
|
||||
"error-outline": ErrorOutline,
|
||||
warning: Warning,
|
||||
clock: Clock,
|
||||
darwin: Apple,
|
||||
macOS: Apple,
|
||||
windows: Windows,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import React from "react";
|
||||
|
||||
import { IOsqueryPlatform } from "interfaces/platform";
|
||||
import { OsqueryPlatform } from "interfaces/platform";
|
||||
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
interface IPLatformListItemProps {
|
||||
platform: IOsqueryPlatform;
|
||||
platform: OsqueryPlatform;
|
||||
}
|
||||
|
||||
const baseClassListItem = "platform-list-item";
|
||||
|
|
@ -20,7 +20,7 @@ const PlatformListItem = ({ platform }: IPLatformListItemProps) => {
|
|||
};
|
||||
|
||||
// TODO: remove when freebsd is removed
|
||||
type IPlatformsWithFreebsd = IOsqueryPlatform | "freebsd";
|
||||
type IPlatformsWithFreebsd = OsqueryPlatform | "freebsd";
|
||||
|
||||
interface IQueryTablePlatformsProps {
|
||||
platforms: IPlatformsWithFreebsd[];
|
||||
|
|
@ -38,7 +38,7 @@ const QueryTablePlatforms = ({ platforms }: IQueryTablePlatformsProps) => {
|
|||
return (
|
||||
<PlatformListItem
|
||||
key={platform}
|
||||
platform={platform as IOsqueryPlatform} // TODO: remove when freebsd is removed
|
||||
platform={platform as OsqueryPlatform} // TODO: remove when freebsd is removed
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/controls/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/software/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/queries/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/schedule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/policies/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/settings/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/manage users/i)).toBeInTheDocument();
|
||||
|
|
@ -80,7 +79,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/controls/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/software/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/queries/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/schedule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/policies/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/my account/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/documentation/i)).toBeInTheDocument();
|
||||
|
|
@ -122,7 +120,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/sign out/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText(/controls/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/schedule/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/settings/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -152,7 +149,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/controls/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/software/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/queries/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/schedule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/policies/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/settings/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/manage users/i)).toBeInTheDocument();
|
||||
|
|
@ -189,7 +185,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/controls/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/software/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/queries/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/schedule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/policies/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/my account/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/documentation/i)).toBeInTheDocument();
|
||||
|
|
@ -231,7 +226,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/sign out/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText(/controls/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/schedule/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/settings/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -264,7 +258,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/controls/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/software/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/queries/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/schedule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/policies/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/settings/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/my account/i)).toBeInTheDocument();
|
||||
|
|
@ -302,7 +295,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/controls/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/software/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/queries/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/schedule/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/policies/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/my account/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/documentation/i)).toBeInTheDocument();
|
||||
|
|
@ -344,7 +336,6 @@ describe("SiteTopNav - component", () => {
|
|||
expect(screen.getByText(/sign out/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText(/controls/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/schedule/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/settings/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/manage users/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,13 +39,10 @@ const REGEX_DETAIL_PAGES = {
|
|||
PACK_NEW: /\/packs\/new/i,
|
||||
POLICY_EDIT: /\/policies\/\d+/i,
|
||||
POLICY_NEW: /\/policies\/new/i,
|
||||
QUERY_EDIT: /\/queries\/\d+/i,
|
||||
QUERY_NEW: /\/queries\/new/i,
|
||||
SOFTWARE_DETAILS: /\/software\/\d+/i,
|
||||
};
|
||||
|
||||
const REGEX_GLOBAL_PAGES = {
|
||||
MANAGE_QUERIES: /\/queries\/manage/i,
|
||||
MANAGE_PACKS: /\/packs\/manage/i,
|
||||
ORGANIZATION: /\/settings\/organization/i,
|
||||
USERS: /\/settings\/users/i,
|
||||
|
|
|
|||
|
|
@ -77,14 +77,6 @@ export default (
|
|||
regex: new RegExp(`^${URL_PREFIX}/queries/`),
|
||||
pathname: PATHS.MANAGE_QUERIES,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Schedule",
|
||||
location: {
|
||||
regex: new RegExp(`^${URL_PREFIX}/(schedule|packs)/`),
|
||||
pathname: PATHS.MANAGE_SCHEDULE,
|
||||
},
|
||||
exclude: !isMaintainerOrAdmin,
|
||||
withParams: { type: "query", names: ["team_id"] },
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { find } from "lodash";
|
|||
|
||||
import { osqueryTables } from "utilities/osquery_tables";
|
||||
import { IOsQueryTable, DEFAULT_OSQUERY_TABLE } from "interfaces/osquery_table";
|
||||
import { IPlatformString } from "interfaces/platform";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
|
||||
enum ACTIONS {
|
||||
SET_LAST_EDITED_QUERY_INFO = "SET_LAST_EDITED_QUERY_INFO",
|
||||
|
|
@ -25,7 +25,7 @@ interface ISetLastEditedQueryInfo {
|
|||
lastEditedQueryBody?: string;
|
||||
lastEditedQueryResolution?: string;
|
||||
lastEditedQueryCritical?: boolean;
|
||||
lastEditedQueryPlatform?: IPlatformString | null;
|
||||
lastEditedQueryPlatform?: SelectedPlatformString | null;
|
||||
defaultPolicy?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ type InitialStateType = {
|
|||
lastEditedQueryBody: string;
|
||||
lastEditedQueryResolution: string;
|
||||
lastEditedQueryCritical: boolean;
|
||||
lastEditedQueryPlatform: IPlatformString | null;
|
||||
lastEditedQueryPlatform: SelectedPlatformString | null;
|
||||
defaultPolicy: boolean;
|
||||
setLastEditedQueryId: (value: number) => void;
|
||||
setLastEditedQueryName: (value: string) => void;
|
||||
|
|
@ -63,7 +63,7 @@ type InitialStateType = {
|
|||
setLastEditedQueryBody: (value: string) => void;
|
||||
setLastEditedQueryResolution: (value: string) => void;
|
||||
setLastEditedQueryCritical: (value: boolean) => void;
|
||||
setLastEditedQueryPlatform: (value: IPlatformString | null) => void;
|
||||
setLastEditedQueryPlatform: (value: SelectedPlatformString | null) => void;
|
||||
setDefaultPolicy: (value: boolean) => void;
|
||||
policyTeamId: number;
|
||||
setPolicyTeamId: (id: number) => void;
|
||||
|
|
@ -210,7 +210,7 @@ const PolicyProvider = ({ children }: Props): JSX.Element => {
|
|||
[]
|
||||
);
|
||||
const setLastEditedQueryPlatform = useCallback(
|
||||
(lastEditedQueryPlatform: IPlatformString | null | undefined) => {
|
||||
(lastEditedQueryPlatform: SelectedPlatformString | null | undefined) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryPlatform,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { find } from "lodash";
|
|||
import { osqueryTables } from "utilities/osquery_tables";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
|
|
@ -16,11 +18,19 @@ type InitialStateType = {
|
|||
lastEditedQueryDescription: string;
|
||||
lastEditedQueryBody: string;
|
||||
lastEditedQueryObserverCanRun: boolean;
|
||||
lastEditedQueryFrequency: number;
|
||||
lastEditedQueryPlatforms: SelectedPlatformString;
|
||||
lastEditedQueryMinOsqueryVersion: string;
|
||||
lastEditedQueryLoggingType: QueryLoggingOption;
|
||||
setLastEditedQueryId: (value: number) => void;
|
||||
setLastEditedQueryName: (value: string) => void;
|
||||
setLastEditedQueryDescription: (value: string) => void;
|
||||
setLastEditedQueryBody: (value: string) => void;
|
||||
setLastEditedQueryObserverCanRun: (value: boolean) => void;
|
||||
setLastEditedQueryFrequency: (value: number) => void;
|
||||
setLastEditedQueryPlatforms: (value: SelectedPlatformString) => void;
|
||||
setLastEditedQueryMinOsqueryVersion: (value: string) => void;
|
||||
setLastEditedQueryLoggingType: (value: string) => void;
|
||||
setSelectedOsqueryTable: (tableName: string) => void;
|
||||
};
|
||||
|
||||
|
|
@ -32,11 +42,19 @@ const initialState = {
|
|||
lastEditedQueryDescription: DEFAULT_QUERY.description,
|
||||
lastEditedQueryBody: DEFAULT_QUERY.query,
|
||||
lastEditedQueryObserverCanRun: DEFAULT_QUERY.observer_can_run,
|
||||
lastEditedQueryFrequency: DEFAULT_QUERY.interval,
|
||||
lastEditedQueryPlatforms: DEFAULT_QUERY.platform,
|
||||
lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version,
|
||||
lastEditedQueryLoggingType: DEFAULT_QUERY.logging,
|
||||
setLastEditedQueryId: () => null,
|
||||
setLastEditedQueryName: () => null,
|
||||
setLastEditedQueryDescription: () => null,
|
||||
setLastEditedQueryBody: () => null,
|
||||
setLastEditedQueryObserverCanRun: () => null,
|
||||
setLastEditedQueryFrequency: () => null,
|
||||
setLastEditedQueryPlatforms: () => null,
|
||||
setLastEditedQueryMinOsqueryVersion: () => null,
|
||||
setLastEditedQueryLoggingType: () => null,
|
||||
setSelectedOsqueryTable: () => null,
|
||||
};
|
||||
|
||||
|
|
@ -77,6 +95,22 @@ const reducer = (state: InitialStateType, action: any) => {
|
|||
typeof action.lastEditedQueryObserverCanRun === "undefined"
|
||||
? state.lastEditedQueryObserverCanRun
|
||||
: action.lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency:
|
||||
typeof action.lastEditedQueryFrequency === "undefined"
|
||||
? state.lastEditedQueryFrequency
|
||||
: action.lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms:
|
||||
typeof action.lastEditedQueryPlatforms === "undefined"
|
||||
? state.lastEditedQueryPlatforms
|
||||
: action.lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion:
|
||||
typeof action.lastEditedQueryMinOsqueryVersion === "undefined"
|
||||
? state.lastEditedQueryMinOsqueryVersion
|
||||
: action.lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType:
|
||||
typeof action.lastEditedQueryLoggingType === "undefined"
|
||||
? state.lastEditedQueryLoggingType
|
||||
: action.lastEditedQueryLoggingType,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
|
@ -95,6 +129,10 @@ const QueryProvider = ({ children }: Props) => {
|
|||
lastEditedQueryDescription: state.lastEditedQueryDescription,
|
||||
lastEditedQueryBody: state.lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun: state.lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency: state.lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms: state.lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
|
||||
setLastEditedQueryId: (lastEditedQueryId: number) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
|
|
@ -127,6 +165,32 @@ const QueryProvider = ({ children }: Props) => {
|
|||
lastEditedQueryObserverCanRun,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryFrequency: (lastEditedQueryFrequency: number) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryFrequency,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryPlatforms: (lastEditedQueryPlatforms: string) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryPlatforms,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryMinOsqueryVersion: (
|
||||
lastEditedQueryMinOsqueryVersion: string
|
||||
) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
});
|
||||
},
|
||||
setLastEditedQueryLoggingType: (lastEditedQueryLoggingType: string) => {
|
||||
dispatch({
|
||||
type: actions.SET_LAST_EDITED_QUERY_INFO,
|
||||
lastEditedQueryLoggingType,
|
||||
});
|
||||
},
|
||||
setSelectedOsqueryTable: (tableName: string) => {
|
||||
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import { IOsqueryPlatform, SUPPORTED_PLATFORMS } from "interfaces/platform";
|
||||
import { OsqueryPlatform, SUPPORTED_PLATFORMS } from "interfaces/platform";
|
||||
import checkPlatformCompatibility from "utilities/sql_tools";
|
||||
|
||||
import PlatformCompatibility from "components/PlatformCompatibility";
|
||||
|
|
@ -16,7 +16,7 @@ const DEBOUNCE_DELAY = 300;
|
|||
|
||||
const usePlatformCompatibility = (): IPlatformCompatibility => {
|
||||
const [compatiblePlatforms, setCompatiblePlatforms] = useState<
|
||||
IOsqueryPlatform[] | null
|
||||
OsqueryPlatform[] | null
|
||||
>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { forEach } from "lodash";
|
||||
|
||||
import { IPlatformString, SUPPORTED_PLATFORMS } from "interfaces/platform";
|
||||
import {
|
||||
SelectedPlatformString,
|
||||
SUPPORTED_PLATFORMS,
|
||||
} from "interfaces/platform";
|
||||
|
||||
import PlatformSelector from "components/PlatformSelector";
|
||||
|
||||
|
|
@ -13,7 +16,7 @@ export interface IPlatformSelector {
|
|||
}
|
||||
|
||||
const usePlatformSelector = (
|
||||
platformContext: IPlatformString | null | undefined,
|
||||
platformContext: SelectedPlatformString | null | undefined,
|
||||
baseClass = ""
|
||||
): IPlatformSelector => {
|
||||
const [checkDarwin, setCheckDarwin] = useState(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import PropTypes from "prop-types";
|
||||
import { IOsqueryPlatform } from "./platform";
|
||||
import { OsqueryPlatform } from "./platform";
|
||||
|
||||
export default PropTypes.shape({
|
||||
columns: PropTypes.arrayOf(
|
||||
|
|
@ -28,7 +28,7 @@ export interface IQueryTableColumn {
|
|||
hidden: boolean;
|
||||
required: boolean;
|
||||
index: boolean;
|
||||
platforms?: IOsqueryPlatform[];
|
||||
platforms?: OsqueryPlatform[];
|
||||
requires_user_context?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ export interface IOsQueryTable {
|
|||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
platforms: IOsqueryPlatform[];
|
||||
platforms: OsqueryPlatform[];
|
||||
evented: boolean;
|
||||
cacheable: boolean;
|
||||
columns: IQueryTableColumn[];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type IOsqueryPlatform =
|
||||
export type OsqueryPlatform =
|
||||
| "darwin"
|
||||
| "macOS"
|
||||
| "windows"
|
||||
|
|
@ -8,14 +8,17 @@ export type IOsqueryPlatform =
|
|||
| "chrome"
|
||||
| "ChromeOS";
|
||||
|
||||
export type ISelectedPlatform =
|
||||
| "all"
|
||||
| "darwin"
|
||||
| "windows"
|
||||
| "linux"
|
||||
| "chrome";
|
||||
export type SupportedPlatform = "darwin" | "windows" | "linux" | "chrome";
|
||||
|
||||
export type IPlatformString =
|
||||
export const SUPPORTED_PLATFORMS: SupportedPlatform[] = [
|
||||
"darwin",
|
||||
"windows",
|
||||
"linux",
|
||||
"chrome",
|
||||
];
|
||||
export type SelectedPlatform = SupportedPlatform | "all";
|
||||
|
||||
export type SelectedPlatformString =
|
||||
| ""
|
||||
| "darwin"
|
||||
| "windows"
|
||||
|
|
@ -33,15 +36,8 @@ export type IPlatformString =
|
|||
| "windows,chrome"
|
||||
| "linux,chrome";
|
||||
|
||||
export const SUPPORTED_PLATFORMS = [
|
||||
"darwin",
|
||||
"windows",
|
||||
"linux",
|
||||
"chrome",
|
||||
] as const;
|
||||
|
||||
// TODO: revisit this approach pending resolution of https://github.com/fleetdm/fleet/issues/3555.
|
||||
export const MACADMINS_EXTENSION_TABLES: Record<string, IOsqueryPlatform[]> = {
|
||||
export const MACADMINS_EXTENSION_TABLES: Record<string, OsqueryPlatform[]> = {
|
||||
file_lines: ["darwin", "linux", "windows"],
|
||||
filevault_users: ["darwin"],
|
||||
google_chrome_profiles: ["darwin", "linux", "windows"],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import PropTypes from "prop-types";
|
||||
import { IPlatformString } from "interfaces/platform";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
|
||||
// Legacy PropTypes used on host interface
|
||||
export default PropTypes.shape({
|
||||
|
|
@ -31,7 +31,7 @@ export interface IPolicy {
|
|||
author_name: string;
|
||||
author_email: string;
|
||||
resolution: string;
|
||||
platform: IPlatformString;
|
||||
platform: SelectedPlatformString;
|
||||
team_id?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
|
@ -80,7 +80,7 @@ export interface IPolicyFormData {
|
|||
description?: string | number | boolean | undefined;
|
||||
resolution?: string | number | boolean | undefined;
|
||||
critical?: boolean;
|
||||
platform?: IPlatformString;
|
||||
platform?: SelectedPlatformString;
|
||||
name?: string | number | boolean | undefined;
|
||||
query?: string | number | boolean | undefined;
|
||||
team_id?: number;
|
||||
|
|
@ -95,6 +95,6 @@ export interface IPolicyNew {
|
|||
query: string;
|
||||
resolution: string;
|
||||
critical: boolean;
|
||||
platform: IPlatformString;
|
||||
platform: SelectedPlatformString;
|
||||
mdm_required?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,22 @@
|
|||
import PropTypes from "prop-types";
|
||||
import { IFormField } from "./form_field";
|
||||
import packInterface, { IPack } from "./pack";
|
||||
import scheduledQueryStatsInterface, {
|
||||
IScheduledQueryStats,
|
||||
} from "./scheduled_query_stats";
|
||||
import { IPack } from "./pack";
|
||||
import { ISchedulableQuery } from "./schedulable_query";
|
||||
import { IScheduledQueryStats } from "./scheduled_query_stats";
|
||||
|
||||
export default PropTypes.shape({
|
||||
created_at: PropTypes.string,
|
||||
updated_at: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
query: PropTypes.string,
|
||||
saved: PropTypes.bool,
|
||||
author_id: PropTypes.number,
|
||||
author_name: PropTypes.string,
|
||||
observer_can_run: PropTypes.bool,
|
||||
packs: PropTypes.arrayOf(packInterface),
|
||||
stats: scheduledQueryStatsInterface,
|
||||
});
|
||||
export interface IQueryFormData {
|
||||
description?: string | number | boolean | undefined;
|
||||
name?: string | number | boolean | undefined;
|
||||
query?: string | number | boolean | undefined;
|
||||
observer_can_run?: string | number | boolean | undefined;
|
||||
automations_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IStoredQueryResponse {
|
||||
query: IQuery;
|
||||
query: ISchedulableQuery;
|
||||
}
|
||||
|
||||
export interface IFleetQueriesResponse {
|
||||
queries: IQuery[];
|
||||
queries: ISchedulableQuery[];
|
||||
}
|
||||
|
||||
export interface IQuery {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { IFormField } from "./form_field";
|
||||
import { IPack } from "./pack";
|
||||
import { IPlatformString } from "./platform";
|
||||
import { SelectedPlatformString, SupportedPlatform } from "./platform";
|
||||
|
||||
// Query itself
|
||||
export interface ISchedulableQuery {
|
||||
|
|
@ -12,7 +12,7 @@ export interface ISchedulableQuery {
|
|||
query: string;
|
||||
team_id: number | null;
|
||||
interval: number;
|
||||
platform: IPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted
|
||||
platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted
|
||||
min_osquery_version: string;
|
||||
automations_enabled: boolean;
|
||||
logging: QueryLoggingOption;
|
||||
|
|
@ -24,6 +24,11 @@ export interface ISchedulableQuery {
|
|||
packs: IPack[];
|
||||
stats: ISchedulableQueryStats;
|
||||
}
|
||||
|
||||
export interface IEnhancedQuery extends ISchedulableQuery {
|
||||
performance: string;
|
||||
platforms: SupportedPlatform[];
|
||||
}
|
||||
export interface ISchedulableQueryStats {
|
||||
user_time_p50?: number;
|
||||
user_time_p95?: number;
|
||||
|
|
@ -46,6 +51,10 @@ export interface IListQueriesResponse {
|
|||
queries: ISchedulableQuery[];
|
||||
}
|
||||
|
||||
export interface IQueryKeyQueriesLoadAll {
|
||||
scope: "queries";
|
||||
teamId: number | undefined;
|
||||
}
|
||||
// Create a new query
|
||||
/** POST /api/v1/fleet/queries */
|
||||
export interface ICreateQueryRequestBody {
|
||||
|
|
@ -55,7 +64,7 @@ export interface ICreateQueryRequestBody {
|
|||
observer_can_run?: boolean;
|
||||
team_id?: number; // global query if ommitted
|
||||
interval?: number; // default 0 means never run
|
||||
platform?: IPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted
|
||||
platform?: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted
|
||||
min_osquery_version?: string; // default all versions if ommitted
|
||||
automations_enabled?: boolean; // whether to send data to the configured log destination according to the query's `interval`. Default false if ommitted.
|
||||
logging?: QueryLoggingOption;
|
||||
|
|
@ -67,9 +76,14 @@ export interface ICreateQueryRequestBody {
|
|||
/** PATCH /api/v1/fleet/queries/{id} */
|
||||
export interface IModifyQueryRequestBody
|
||||
extends Omit<ICreateQueryRequestBody, "name" | "query"> {
|
||||
id: number;
|
||||
id?: number;
|
||||
name?: string;
|
||||
query?: string;
|
||||
description?: string;
|
||||
observer_can_run?: boolean;
|
||||
frequency?: number;
|
||||
platform?: SelectedPlatformString;
|
||||
min_osquery_version?: string;
|
||||
}
|
||||
|
||||
// response is ISchedulableQuery // better way to indicate this?
|
||||
|
|
@ -77,7 +91,7 @@ export interface IModifyQueryRequestBody
|
|||
// Delete a query by name
|
||||
/** DELETE /api/v1/fleet/queries/{name} */
|
||||
export interface IDeleteQueryRequestBody {
|
||||
team_id?: number; // searches for a global query if ommitted
|
||||
team_id?: number; // searches for a global query if omitted
|
||||
}
|
||||
|
||||
// Delete a query by id
|
||||
|
|
@ -100,7 +114,7 @@ export interface IQueryFormFields {
|
|||
query: IFormField<string>;
|
||||
observer_can_run: IFormField<boolean>;
|
||||
frequency: IFormField<number>;
|
||||
platforms: IFormField<IPlatformString>;
|
||||
platforms: IFormField<SelectedPlatformString>;
|
||||
min_osquery_version: IFormField<string>;
|
||||
logging: IFormField<QueryLoggingOption>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
IMdmSolution,
|
||||
IMdmSummaryResponse,
|
||||
} from "interfaces/mdm";
|
||||
import { ISelectedPlatform } from "interfaces/platform";
|
||||
import { SelectedPlatform } from "interfaces/platform";
|
||||
import { ISoftwareResponse, ISoftwareCountResponse } from "interfaces/software";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { useTeamIdParam } from "hooks/useTeamIdParam";
|
||||
|
|
@ -107,7 +107,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
|
|||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<ISelectedPlatform>(
|
||||
const [selectedPlatform, setSelectedPlatform] = useState<SelectedPlatform>(
|
||||
"all"
|
||||
);
|
||||
const [
|
||||
|
|
@ -757,7 +757,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
|
|||
className={`${baseClass}__platform_dropdown`}
|
||||
options={PLATFORM_DROPDOWN_OPTIONS}
|
||||
searchable={false}
|
||||
onChange={(value: ISelectedPlatform) => {
|
||||
onChange={(value: SelectedPlatform) => {
|
||||
const selectedPlatformOption = PLATFORM_DROPDOWN_OPTIONS.find(
|
||||
(platform) => platform.value === value
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import PATHS from "router/paths";
|
|||
|
||||
import labelsAPI from "services/entities/labels";
|
||||
import DataError from "components/DataError";
|
||||
import { ISelectedPlatform } from "interfaces/platform";
|
||||
import { SelectedPlatform } from "interfaces/platform";
|
||||
import { useQuery } from "react-query";
|
||||
import { ILabelSpecResponse } from "interfaces/label";
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ interface IHostSummaryProps {
|
|||
isLoadingHostsSummary: boolean;
|
||||
showHostsUI: boolean;
|
||||
errorHosts: boolean;
|
||||
selectedPlatform?: ISelectedPlatform;
|
||||
selectedPlatform?: SelectedPlatform;
|
||||
}
|
||||
|
||||
const HostsSummary = ({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
OS_END_OF_LIFE_LINK_BY_PLATFORM,
|
||||
OS_VENDOR_BY_PLATFORM,
|
||||
} from "interfaces/operating_system";
|
||||
import { ISelectedPlatform } from "interfaces/platform";
|
||||
import { SelectedPlatform } from "interfaces/platform";
|
||||
import {
|
||||
getOSVersions,
|
||||
IGetOSVersionsQueryKey,
|
||||
|
|
@ -26,7 +26,7 @@ import generateTableHeaders from "./OperatingSystemsTableConfig";
|
|||
|
||||
interface IOperatingSystemsCardProps {
|
||||
currentTeamId: number | undefined;
|
||||
selectedPlatform: ISelectedPlatform;
|
||||
selectedPlatform: SelectedPlatform;
|
||||
showTitle: boolean;
|
||||
/** controls the displaying of description text under the title. Defaults to `true` */
|
||||
showDescription?: boolean;
|
||||
|
|
@ -42,7 +42,7 @@ const DEFAULT_SORT_HEADER = "hosts_count";
|
|||
const PAGE_SIZE = 8;
|
||||
const baseClass = "operating-systems";
|
||||
|
||||
const EmptyOperatingSystems = (platform: ISelectedPlatform): JSX.Element => (
|
||||
const EmptyOperatingSystems = (platform: SelectedPlatform): JSX.Element => (
|
||||
<EmptyTable
|
||||
className={`${baseClass}__os-empty-table`}
|
||||
header={`No${
|
||||
|
|
|
|||
|
|
@ -268,7 +268,6 @@ const allHostTableHeaders: IDataColumn[] = [
|
|||
Cell: (cellProps: ICellProps) => {
|
||||
const value = cellProps.cell.value;
|
||||
const tooltip = {
|
||||
id: cellProps.row.original.id,
|
||||
tooltipText: getHostStatusTooltipText(value),
|
||||
};
|
||||
return <StatusIndicator value={value} tooltip={tooltip} />;
|
||||
|
|
|
|||
|
|
@ -54,22 +54,6 @@ describe("Host Actions Dropdown", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("renders the Query action as disabled if the host is offline", async () => {
|
||||
const render = createCustomRenderer();
|
||||
|
||||
const { user } = render(
|
||||
<HostActionsDropdown
|
||||
onSelect={noop}
|
||||
hostStatus="offline"
|
||||
hostMdmEnrollemntStatus="On (automatic)"
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Actions"));
|
||||
|
||||
expect(screen.getByText("Query").parentNode).toHaveClass("is-disabled");
|
||||
});
|
||||
|
||||
it("renders the Show Disk Encryption Key action when on premium tier and we store the disk encryption key", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
|
|
|
|||
|
|
@ -18,16 +18,20 @@ import {
|
|||
IHost,
|
||||
IDeviceMappingResponse,
|
||||
IMacadminsResponse,
|
||||
IPackStats,
|
||||
IHostResponse,
|
||||
IHostMdmData,
|
||||
IPackStats,
|
||||
} from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { IHostPolicy } from "interfaces/policy";
|
||||
import { IQuery, IFleetQueriesResponse } from "interfaces/query";
|
||||
import { IQueryStats } from "interfaces/query_stats";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import {
|
||||
IListQueriesResponse,
|
||||
IQueryKeyQueriesLoadAll,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import TabsWrapper from "components/TabsWrapper";
|
||||
|
|
@ -48,7 +52,6 @@ import UsersCard from "../cards/Users";
|
|||
import PoliciesCard from "../cards/Policies";
|
||||
import ScheduleCard from "../cards/Schedule";
|
||||
import PacksCard from "../cards/Packs";
|
||||
import SelectQueryModal from "./modals/SelectQueryModal";
|
||||
import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal";
|
||||
import OSPolicyModal from "./modals/OSPolicyModal";
|
||||
import UnenrollMdmModal from "./modals/UnenrollMdmModal";
|
||||
|
|
@ -61,6 +64,7 @@ import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal";
|
|||
import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
|
||||
import MacSettingsModal from "../MacSettingsModal";
|
||||
import BootstrapPackageModal from "./modals/BootstrapPackageModal";
|
||||
import SelectQueryModal from "./modals/SelectQueryModal";
|
||||
|
||||
const baseClass = "host-details";
|
||||
|
||||
|
|
@ -151,24 +155,25 @@ const HostDetailsPage = ({
|
|||
|
||||
const [refetchStartTime, setRefetchStartTime] = useState<number | null>(null);
|
||||
const [showRefetchSpinner, setShowRefetchSpinner] = useState(false);
|
||||
const [packsState, setPacksState] = useState<IPackStats[]>();
|
||||
const [schedule, setSchedule] = useState<IQueryStats[]>();
|
||||
const [packsState, setPacksState] = useState<IPackStats[]>();
|
||||
const [hostSoftware, setHostSoftware] = useState<ISoftware[]>([]);
|
||||
const [usersState, setUsersState] = useState<{ username: string }[]>([]);
|
||||
const [usersSearchString, setUsersSearchString] = useState("");
|
||||
const [pathname, setPathname] = useState("");
|
||||
|
||||
const { data: fleetQueries, error: fleetQueriesError } = useQuery<
|
||||
IFleetQueriesResponse,
|
||||
IListQueriesResponse,
|
||||
Error,
|
||||
IQuery[]
|
||||
>("fleet queries", () => queryAPI.loadAll(), {
|
||||
ISchedulableQuery[],
|
||||
IQueryKeyQueriesLoadAll[]
|
||||
>([{ scope: "queries", teamId: undefined }], () => queryAPI.loadAll(), {
|
||||
enabled: !!hostIdFromURL,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
select: (data: IFleetQueriesResponse) => data.queries,
|
||||
select: (data: IListQueriesResponse) => data.queries,
|
||||
});
|
||||
|
||||
const { data: teams } = useQuery<ILoadTeamsResponse, Error, ITeam[]>(
|
||||
|
|
@ -312,8 +317,8 @@ const HostDetailsPage = ({
|
|||
},
|
||||
{ packs: [], schedule: [] }
|
||||
);
|
||||
setPacksState(packStatsByType.packs);
|
||||
setSchedule(packStatsByType.schedule);
|
||||
setPacksState(packStatsByType.packs);
|
||||
}
|
||||
},
|
||||
onError: (error) => handlePageError(error),
|
||||
|
|
@ -484,9 +489,9 @@ const HostDetailsPage = ({
|
|||
router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id));
|
||||
};
|
||||
|
||||
const onQueryHostSaved = (selectedQuery: IQuery) => {
|
||||
const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => {
|
||||
router.push(
|
||||
PATHS.EDIT_QUERY(selectedQuery) +
|
||||
PATHS.EDIT_QUERY(selectedQuery.id) +
|
||||
TAGGED_TEMPLATES.queryByHostRoute(host?.id)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useCallback, useContext } from "react";
|
||||
|
||||
import { filter, includes } from "lodash";
|
||||
import { IQuery } from "interfaces/query";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
@ -11,12 +10,13 @@ import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
|
|||
|
||||
import DataError from "components/DataError";
|
||||
import permissions from "utilities/permissions";
|
||||
import { ISchedulableQuery } from "interfaces/schedulable_query";
|
||||
|
||||
export interface ISelectQueryModalProps {
|
||||
onCancel: () => void;
|
||||
onQueryHostCustom: () => void;
|
||||
onQueryHostSaved: (selectedQuery: IQuery) => void;
|
||||
queries: IQuery[] | [];
|
||||
onQueryHostSaved: (selectedQuery: ISchedulableQuery) => void;
|
||||
queries: ISchedulableQuery[] | [];
|
||||
queryErrors: Error | null;
|
||||
isOnlyObserver?: boolean;
|
||||
hostsTeamId: number | null;
|
||||
|
|
|
|||
|
|
@ -187,7 +187,6 @@ const HostSummary = ({
|
|||
<StatusIndicator
|
||||
value={status || ""} // temporary work around of integration test bug
|
||||
tooltip={{
|
||||
id,
|
||||
tooltipText: getHostStatusTooltipText(status),
|
||||
position: "bottom",
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ const generatePackTableHeaders = (): IDataColumn[] => {
|
|||
<PillCell
|
||||
value={cellProps.cell.value}
|
||||
customIdPrefix="query-perf-pill"
|
||||
hostDetails
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const TAGGED_TEMPLATES = {
|
|||
};
|
||||
|
||||
const DEFAULT_SORT_DIRECTION = "asc";
|
||||
const DEFAULT_SORT_HEADER = "updated_at";
|
||||
const DEFAULT_SORT_HEADER = "name";
|
||||
|
||||
interface IPoliciesTableProps {
|
||||
policiesList: IPolicyStats[];
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import PATHS from "router/paths";
|
|||
import sortUtils from "utilities/sort";
|
||||
import { PolicyResponse } from "utilities/constants";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import PassingColumnHeader from "../PassingColumnHeader";
|
||||
|
||||
interface IGetToggleAllRowsSelectedProps {
|
||||
|
|
@ -138,7 +139,7 @@ const generateTableHeaders = (
|
|||
type="dark"
|
||||
effect="solid"
|
||||
id={`critical-tooltip-${cellProps.row.original.id}`}
|
||||
backgroundColor="#3e4771"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
This policy has been marked as critical.
|
||||
{isSandboxMode && (
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import usePlatformCompatibility from "hooks/usePlatformCompatibility";
|
|||
import usePlatformSelector from "hooks/usePlatformSelector";
|
||||
|
||||
import { IPolicy, IPolicyFormData } from "interfaces/policy";
|
||||
import { IOsqueryPlatform, IPlatformString } from "interfaces/platform";
|
||||
import { OsqueryPlatform, SelectedPlatformString } from "interfaces/platform";
|
||||
import { DEFAULT_POLICIES } from "pages/policies/constants";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
|
|
@ -226,7 +226,7 @@ const PolicyForm = ({
|
|||
});
|
||||
}
|
||||
|
||||
let selectedPlatforms: IOsqueryPlatform[] = [];
|
||||
let selectedPlatforms: OsqueryPlatform[] = [];
|
||||
if (isEditMode || defaultPolicy) {
|
||||
selectedPlatforms = getSelectedPlatforms();
|
||||
} else {
|
||||
|
|
@ -234,7 +234,9 @@ const PolicyForm = ({
|
|||
setSelectedPlatforms(selectedPlatforms);
|
||||
}
|
||||
|
||||
const newPlatformString = selectedPlatforms.join(",") as IPlatformString;
|
||||
const newPlatformString = selectedPlatforms.join(
|
||||
","
|
||||
) as SelectedPlatformString;
|
||||
|
||||
if (!defaultPolicy) {
|
||||
setLastEditedQueryPlatform(newPlatformString);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { AppContext } from "context/app";
|
|||
import { PolicyContext } from "context/policy";
|
||||
import { IPlatformSelector } from "hooks/usePlatformSelector";
|
||||
import { IPolicyFormData } from "interfaces/policy";
|
||||
import { IPlatformString } from "interfaces/platform";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
|
||||
// @ts-ignore
|
||||
|
|
@ -81,7 +81,7 @@ const SaveNewPolicyModal = ({
|
|||
|
||||
const newPlatformString = platformSelector
|
||||
.getSelectedPlatforms()
|
||||
.join(",") as IPlatformString;
|
||||
.join(",") as SelectedPlatformString;
|
||||
setLastEditedQueryPlatform(newPlatformString);
|
||||
|
||||
const { valid: validName, errors: newErrors } = validatePolicyName(name);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { IPolicyNew } from "interfaces/policy";
|
||||
import { IPlatformString } from "interfaces/platform";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
|
||||
const DEFAULT_POLICY_PLATFORM: IPlatformString = "";
|
||||
const DEFAULT_POLICY_PLATFORM: SelectedPlatformString = "";
|
||||
|
||||
export const DEFAULT_POLICY = {
|
||||
id: 1,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import React, {
|
|||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { RouteProps, InjectedRouter } from "react-router";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useQuery } from "react-query";
|
||||
import { pick } from "lodash";
|
||||
|
||||
|
|
@ -13,48 +13,52 @@ import { AppContext } from "context/app";
|
|||
import { TableContext } from "context/table";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { performanceIndicator } from "utilities/helpers";
|
||||
import { IOsqueryPlatform } from "interfaces/platform";
|
||||
import { IQuery, IFleetQueriesResponse } from "interfaces/query";
|
||||
import fleetQueriesAPI from "services/entities/queries";
|
||||
import { SupportedPlatform } from "interfaces/platform";
|
||||
import { API_ALL_TEAMS_ID } from "interfaces/team";
|
||||
import {
|
||||
IEnhancedQuery,
|
||||
IQueryKeyQueriesLoadAll,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import queriesAPI from "services/entities/queries";
|
||||
import PATHS from "router/paths";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import checkPlatformCompatibility from "utilities/sql_tools";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
import TableDataError from "components/DataError";
|
||||
import MainContent from "components/MainContent";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import QueriesTable from "./components/QueriesTable";
|
||||
import DeleteQueryModal from "./components/DeleteQueryModal";
|
||||
import ManageAutomationsModal from "./components/ManageAutomationsModal/ManageAutomationsModal";
|
||||
import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal";
|
||||
|
||||
const baseClass = "manage-queries-page";
|
||||
interface IManageQueriesPageProps {
|
||||
route: RouteProps;
|
||||
router: InjectedRouter; // v3
|
||||
location: {
|
||||
pathname?: string;
|
||||
pathname: string;
|
||||
query: {
|
||||
platform?: string;
|
||||
page?: string;
|
||||
query?: string;
|
||||
order_key?: string;
|
||||
order_direction?: "asc" | "desc";
|
||||
team_id?: string;
|
||||
};
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IQueryTableData extends IQuery {
|
||||
performance: string;
|
||||
platforms: string[];
|
||||
}
|
||||
|
||||
const getPlatforms = (queryString: string): Array<IOsqueryPlatform | "---"> => {
|
||||
const getPlatforms = (queryString: string): SupportedPlatform[] => {
|
||||
const { platforms } = checkPlatformCompatibility(queryString);
|
||||
|
||||
return platforms || [DEFAULT_EMPTY_CELL_VALUE];
|
||||
return platforms ?? [];
|
||||
};
|
||||
|
||||
const enhanceQuery = (q: IQuery) => {
|
||||
const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => {
|
||||
return {
|
||||
...q,
|
||||
performance: performanceIndicator(
|
||||
|
|
@ -71,51 +75,101 @@ const ManageQueriesPage = ({
|
|||
const queryParams = location.query;
|
||||
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isTeamAdmin,
|
||||
isOnlyObserver,
|
||||
isObserverPlus,
|
||||
isAnyTeamObserverPlus,
|
||||
isOnGlobalTeam,
|
||||
setFilteredQueriesPath,
|
||||
filteredQueriesPath,
|
||||
isPremiumTier,
|
||||
isSandboxMode,
|
||||
config,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const { setResetSelectedRows } = useContext(TableContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [queriesList, setQueriesList] = useState<IQueryTableData[] | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
userTeams,
|
||||
currentTeamId,
|
||||
handleTeamChange,
|
||||
teamIdForApi,
|
||||
isRouteOk,
|
||||
} = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
});
|
||||
|
||||
const isAnyTeamSelected = currentTeamId !== -1;
|
||||
|
||||
const [selectedQueryIds, setSelectedQueryIds] = useState<number[]>([]);
|
||||
const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false);
|
||||
const [showManageAutomationsModal, setShowManageAutomationsModal] = useState(
|
||||
false
|
||||
);
|
||||
const [showPreviewDataModal, setShowPreviewDataModal] = useState(false);
|
||||
const [isUpdatingQueries, setIsUpdatingQueries] = useState(false);
|
||||
const [showInheritedQueries, setShowInheritedQueries] = useState(false);
|
||||
const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false);
|
||||
|
||||
const {
|
||||
data: fleetQueries,
|
||||
error: fleetQueriesError,
|
||||
isFetching: isFetchingFleetQueries,
|
||||
refetch: refetchFleetQueries,
|
||||
} = useQuery<IFleetQueriesResponse, Error, IQuery[]>(
|
||||
"fleet queries by platform",
|
||||
() => fleetQueriesAPI.loadAll(),
|
||||
data: curTeamEnhancedQueries,
|
||||
error: curTeamQueriesError,
|
||||
isFetching: isFetchingCurTeamQueries,
|
||||
refetch: refetchCurTeamQueries,
|
||||
} = useQuery<
|
||||
IEnhancedQuery[],
|
||||
Error,
|
||||
IEnhancedQuery[],
|
||||
IQueryKeyQueriesLoadAll[]
|
||||
>(
|
||||
[{ scope: "queries", teamId: teamIdForApi }],
|
||||
({ queryKey: [{ teamId }] }) =>
|
||||
queriesAPI.loadAll(teamId).then(({ queries }) => {
|
||||
return queries.map(enhanceQuery);
|
||||
}),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data: IFleetQueriesResponse) => data.queries,
|
||||
enabled: isRouteOk,
|
||||
staleTime: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const enhancedQueriesList = useMemo(() => {
|
||||
const enhancedQueries = fleetQueries?.map((q: IQuery) => {
|
||||
const query = enhanceQuery(q);
|
||||
return query;
|
||||
});
|
||||
|
||||
return enhancedQueries || [];
|
||||
}, [fleetQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingFleetQueries && enhancedQueriesList) {
|
||||
setQueriesList(enhancedQueriesList);
|
||||
// If a team is selected, inherit global queries
|
||||
const {
|
||||
data: globalEnhancedQueries,
|
||||
error: globalQueriesError,
|
||||
isFetching: isFetchingGlobalQueries,
|
||||
refetch: refetchGlobalQueries,
|
||||
} = useQuery<
|
||||
IEnhancedQuery[],
|
||||
Error,
|
||||
IEnhancedQuery[],
|
||||
IQueryKeyQueriesLoadAll[]
|
||||
>(
|
||||
[{ scope: "queries", teamId: API_ALL_TEAMS_ID }],
|
||||
({ queryKey: [{ teamId }] }) =>
|
||||
queriesAPI.loadAll(teamId).then(({ queries }) => {
|
||||
return queries.map(enhanceQuery);
|
||||
}),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: isRouteOk && isAnyTeamSelected,
|
||||
staleTime: 5000,
|
||||
}
|
||||
}, [enhancedQueriesList, isFetchingFleetQueries]);
|
||||
);
|
||||
|
||||
const automatedQueryIds = useMemo(() => {
|
||||
return curTeamEnhancedQueries
|
||||
? curTeamEnhancedQueries
|
||||
.filter((query) => query.automations_enabled)
|
||||
.map((query) => query.id)
|
||||
: [];
|
||||
}, [curTeamEnhancedQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
const path = location.pathname + location.search;
|
||||
|
|
@ -124,7 +178,7 @@ const ManageQueriesPage = ({
|
|||
}
|
||||
}, [location, filteredQueriesPath, setFilteredQueriesPath]);
|
||||
|
||||
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY);
|
||||
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY(currentTeamId));
|
||||
|
||||
const toggleDeleteQueryModal = useCallback(() => {
|
||||
setShowDeleteQueryModal(!showDeleteQueryModal);
|
||||
|
|
@ -135,80 +189,205 @@ const ManageQueriesPage = ({
|
|||
setSelectedQueryIds(selectedTableQueryIds);
|
||||
};
|
||||
|
||||
const onDeleteQuerySubmit = useCallback(async () => {
|
||||
const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries";
|
||||
const refetchAllQueries = useCallback(() => {
|
||||
refetchCurTeamQueries();
|
||||
refetchGlobalQueries();
|
||||
}, [refetchCurTeamQueries, refetchGlobalQueries]);
|
||||
|
||||
const toggleManageAutomationsModal = useCallback(() => {
|
||||
setShowManageAutomationsModal(!showManageAutomationsModal);
|
||||
}, [showManageAutomationsModal, setShowManageAutomationsModal]);
|
||||
|
||||
const onManageAutomationsClick = () => {
|
||||
toggleManageAutomationsModal();
|
||||
};
|
||||
|
||||
const togglePreviewDataModal = useCallback(() => {
|
||||
// Manage automation modal must close/open every time preview data modal opens/closes
|
||||
setShowManageAutomationsModal(!showManageAutomationsModal);
|
||||
setShowPreviewDataModal(!showPreviewDataModal);
|
||||
}, [
|
||||
showPreviewDataModal,
|
||||
setShowPreviewDataModal,
|
||||
showManageAutomationsModal,
|
||||
setShowManageAutomationsModal,
|
||||
]);
|
||||
|
||||
const onDeleteQuerySubmit = useCallback(async () => {
|
||||
const bulk = selectedQueryIds.length > 1;
|
||||
setIsUpdatingQueries(true);
|
||||
|
||||
const deleteQueries = selectedQueryIds.map((id) =>
|
||||
fleetQueriesAPI.destroy(id)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(deleteQueries).then(() => {
|
||||
renderFlash("success", `Successfully deleted ${queryOrQueries}.`);
|
||||
setResetSelectedRows(true);
|
||||
refetchFleetQueries();
|
||||
});
|
||||
renderFlash("success", `Successfully deleted ${queryOrQueries}.`);
|
||||
if (bulk) {
|
||||
await queriesAPI.bulkDestroy(selectedQueryIds);
|
||||
} else {
|
||||
await queriesAPI.destroy(selectedQueryIds[0]);
|
||||
}
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully deleted ${bulk ? "queries" : "query"}.`
|
||||
);
|
||||
setResetSelectedRows(true);
|
||||
refetchAllQueries();
|
||||
} catch (errorResponse) {
|
||||
renderFlash(
|
||||
"error",
|
||||
`There was an error deleting your ${queryOrQueries}. Please try again later.`
|
||||
`There was an error deleting your ${
|
||||
bulk ? "queries" : "query"
|
||||
}. Please try again later.`
|
||||
);
|
||||
} finally {
|
||||
toggleDeleteQueryModal();
|
||||
setIsUpdatingQueries(false);
|
||||
}
|
||||
}, [refetchFleetQueries, selectedQueryIds, toggleDeleteQueryModal]);
|
||||
}, [refetchAllQueries, selectedQueryIds, toggleDeleteQueryModal]);
|
||||
|
||||
const isTableDataLoading = isFetchingFleetQueries || queriesList === null;
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<h1 className={`${baseClass}__title`}>
|
||||
<span>Queries</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) &&
|
||||
!!fleetQueries?.length && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
Create new query
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>Manage queries to ask specific questions about your devices.</p>
|
||||
</div>
|
||||
<div>
|
||||
{isTableDataLoading && !fleetQueriesError && <Spinner />}
|
||||
{!isTableDataLoading && fleetQueriesError ? (
|
||||
<TableDataError />
|
||||
) : (
|
||||
<QueriesTable
|
||||
queriesList={queriesList}
|
||||
isLoading={isTableDataLoading}
|
||||
onCreateQueryClick={onCreateQueryClick}
|
||||
onDeleteQueryClick={onDeleteQueryClick}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
isObserverPlus={isObserverPlus}
|
||||
isAnyTeamObserverPlus={isAnyTeamObserverPlus || false}
|
||||
router={router}
|
||||
queryParams={queryParams}
|
||||
const renderHeader = () => {
|
||||
if (isPremiumTier) {
|
||||
if (userTeams) {
|
||||
if (userTeams.length > 1 || isOnGlobalTeam) {
|
||||
return (
|
||||
<TeamsDropdown
|
||||
currentUserTeams={userTeams}
|
||||
selectedTeamId={currentTeamId}
|
||||
onChange={handleTeamChange}
|
||||
isSandboxMode={isSandboxMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (!isOnGlobalTeam && userTeams.length === 1) {
|
||||
return <h1>{userTeams[0].name}</h1>;
|
||||
}
|
||||
}
|
||||
}
|
||||
return <h1>Queries</h1>;
|
||||
};
|
||||
|
||||
const renderCurrentScopeQueriesTable = () => {
|
||||
if (isFetchingCurTeamQueries) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (curTeamQueriesError) {
|
||||
return <TableDataError />;
|
||||
}
|
||||
return (
|
||||
<QueriesTable
|
||||
queriesList={curTeamEnhancedQueries || []}
|
||||
isLoading={isFetchingCurTeamQueries}
|
||||
onCreateQueryClick={onCreateQueryClick}
|
||||
onDeleteQueryClick={onDeleteQueryClick}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
isObserverPlus={isObserverPlus}
|
||||
isAnyTeamObserverPlus={isAnyTeamObserverPlus || false}
|
||||
router={router}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderShowInheritedQueriesTableButton = () => {
|
||||
const inheritedQueryCount = globalEnhancedQueries?.length;
|
||||
return (
|
||||
<RevealButton
|
||||
isShowing={showInheritedQueries}
|
||||
className={baseClass}
|
||||
hideText={`Hide ${inheritedQueryCount} inherited quer${
|
||||
inheritedQueryCount === 1 ? "y" : "ies"
|
||||
}`}
|
||||
showText={`Show ${inheritedQueryCount} inherited quer${
|
||||
inheritedQueryCount === 1 ? "y" : "ies"
|
||||
}`}
|
||||
caretPosition={"before"}
|
||||
tooltipHtml={
|
||||
'Queries from the "All teams"<br/>schedule run on this team’s hosts.'
|
||||
}
|
||||
onClick={() => {
|
||||
setShowInheritedQueries(!showInheritedQueries);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderInheritedQueriesTable = () => {
|
||||
if (isFetchingGlobalQueries) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (globalQueriesError) {
|
||||
return <TableDataError />;
|
||||
}
|
||||
return (
|
||||
<QueriesTable
|
||||
queriesList={globalEnhancedQueries || []}
|
||||
isLoading={isFetchingGlobalQueries}
|
||||
onCreateQueryClick={onCreateQueryClick}
|
||||
onDeleteQueryClick={onDeleteQueryClick}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
isObserverPlus={isObserverPlus}
|
||||
isAnyTeamObserverPlus={isAnyTeamObserverPlus || false}
|
||||
router={router}
|
||||
queryParams={queryParams}
|
||||
isInherited
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderInheritedQueriesSection = () => {
|
||||
return (
|
||||
<>
|
||||
{renderShowInheritedQueriesTableButton()}
|
||||
{showInheritedQueries && renderInheritedQueriesTable()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveQueryAutomations = useCallback(
|
||||
async (newAutomatedQueryIds) => {
|
||||
setIsUpdatingAutomations(true);
|
||||
|
||||
// Query ids added to turn on automations
|
||||
const turnOnAutomations = newAutomatedQueryIds.filter(
|
||||
(query: number) => !automatedQueryIds.includes(query)
|
||||
);
|
||||
// Query ids removed to turn off automations
|
||||
const turnOffAutomations = automatedQueryIds.filter(
|
||||
(query: number) => !newAutomatedQueryIds.includes(query)
|
||||
);
|
||||
|
||||
// Update query automations using queries/{id} manage_automations parameter
|
||||
const updateAutomatedQueries: Promise<any>[] = [];
|
||||
turnOnAutomations.map((id: number) =>
|
||||
updateAutomatedQueries.push(
|
||||
queriesAPI.update(id, { automations_enabled: true })
|
||||
)
|
||||
);
|
||||
turnOffAutomations.map((id: number) =>
|
||||
updateAutomatedQueries.push(
|
||||
queriesAPI.update(id, { automations_enabled: false })
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(updateAutomatedQueries).then(() => {
|
||||
renderFlash("success", `Successfully updated query automations.`);
|
||||
refetchAllQueries();
|
||||
});
|
||||
} catch (errorResponse) {
|
||||
renderFlash(
|
||||
"error",
|
||||
`There was an error updating your query automations. Please try again later.`
|
||||
);
|
||||
} finally {
|
||||
toggleManageAutomationsModal();
|
||||
setIsUpdatingAutomations(false);
|
||||
}
|
||||
},
|
||||
[refetchAllQueries, automatedQueryIds, toggleManageAutomationsModal]
|
||||
);
|
||||
|
||||
// const isTableDataLoading = isFetchingFleetQueries || queriesList === null;
|
||||
|
||||
const renderModals = () => {
|
||||
return (
|
||||
<>
|
||||
{showDeleteQueryModal && (
|
||||
<DeleteQueryModal
|
||||
isUpdatingQueries={isUpdatingQueries}
|
||||
|
|
@ -216,6 +395,69 @@ const ManageQueriesPage = ({
|
|||
onSubmit={onDeleteQuerySubmit}
|
||||
/>
|
||||
)}
|
||||
{showManageAutomationsModal && (
|
||||
<ManageAutomationsModal
|
||||
isUpdatingAutomations={isUpdatingAutomations}
|
||||
handleSubmit={onSaveQueryAutomations}
|
||||
onCancel={toggleManageAutomationsModal}
|
||||
togglePreviewDataModal={togglePreviewDataModal}
|
||||
availableQueries={curTeamEnhancedQueries}
|
||||
automatedQueryIds={automatedQueryIds}
|
||||
logDestination={config?.logging.result.plugin || ""}
|
||||
/>
|
||||
)}
|
||||
{showPreviewDataModal && (
|
||||
<PreviewDataModal onCancel={togglePreviewDataModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<div className={`${baseClass}__title`}>{renderHeader()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{(isGlobalAdmin || isTeamAdmin) && (
|
||||
<Button
|
||||
onClick={onManageAutomationsClick}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
variant="inverse"
|
||||
>
|
||||
Manage automations
|
||||
</Button>
|
||||
)}
|
||||
{(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) &&
|
||||
!!curTeamEnhancedQueries?.length && (
|
||||
<>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
Add query
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>
|
||||
Manage and schedule queries to ask questions and collect telemetry
|
||||
for all hosts{isAnyTeamSelected && " assigned to this team"}.
|
||||
</p>
|
||||
</div>
|
||||
{renderCurrentScopeQueriesTable()}
|
||||
{isAnyTeamSelected &&
|
||||
globalEnhancedQueries &&
|
||||
globalEnhancedQueries?.length > 0 &&
|
||||
renderInheritedQueriesSection()}
|
||||
{renderModals()}
|
||||
</div>
|
||||
</MainContent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -56,14 +56,17 @@
|
|||
|
||||
&__action-button-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-field--dropdown {
|
||||
margin: 0;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
.queries-table {
|
||||
.controls {
|
||||
.form-field {
|
||||
&--dropdown {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__platform-dropdown {
|
||||
width: 159px;
|
||||
|
||||
|
|
@ -95,99 +98,100 @@
|
|||
}
|
||||
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
thead {
|
||||
.name__header {
|
||||
width: auto;
|
||||
}
|
||||
.platforms__header {
|
||||
width: $col-sm;
|
||||
}
|
||||
.author_name__header {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
.updated_at__header {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
@media (min-width: $break-md) {
|
||||
.author_name__header {
|
||||
display: table-cell;
|
||||
.data-table {
|
||||
&__wrapper {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
&__table {
|
||||
thead {
|
||||
.name__header {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@media (min-width: $break-lg) {
|
||||
.author_name__header {
|
||||
width: $col-md;
|
||||
.platforms__header {
|
||||
width: $col-sm;
|
||||
}
|
||||
.updated_at__header {
|
||||
display: table-cell;
|
||||
width: auto;
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
.name__cell {
|
||||
max-width: $col-lg;
|
||||
|
||||
.children-wrapper {
|
||||
display: flex;
|
||||
gap: $pad-xsmall;
|
||||
|
||||
.observer-can-run-tooltip {
|
||||
font-weight: $regular;
|
||||
.performance__header {
|
||||
display: none;
|
||||
width: 0;
|
||||
@media (min-width: $break-md) {
|
||||
display: table-cell;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
@media (min-width: $break-lg) {
|
||||
.author_name__header {
|
||||
width: $col-md;
|
||||
}
|
||||
.updated_at__header {
|
||||
display: table-cell;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
tbody {
|
||||
.name__cell {
|
||||
.w400 {
|
||||
max-width: calc(400px - 81px);
|
||||
max-width: $col-lg;
|
||||
|
||||
.children-wrapper {
|
||||
display: flex;
|
||||
gap: $pad-xsmall;
|
||||
|
||||
.observer-can-run-tooltip {
|
||||
font-weight: $regular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.platforms__cell {
|
||||
max-width: $col-md;
|
||||
}
|
||||
.author_name__cell {
|
||||
display: none;
|
||||
max-width: $col-md;
|
||||
img,
|
||||
div,
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
.name__cell {
|
||||
.w400 {
|
||||
max-width: calc(400px - 81px);
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
padding-right: $pad-small;
|
||||
.platforms__cell {
|
||||
max-width: $col-md;
|
||||
}
|
||||
.author-name {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.updated_at__cell {
|
||||
display: none;
|
||||
max-width: $col-md;
|
||||
}
|
||||
@media (min-width: $break-md) {
|
||||
.author_name__cell {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
@media (min-width: $break-lg) {
|
||||
.updated_at__cell {
|
||||
display: table-cell;
|
||||
display: none;
|
||||
max-width: $col-md;
|
||||
}
|
||||
.performance__cell {
|
||||
display: none;
|
||||
max-width: $col-md;
|
||||
}
|
||||
@media (min-width: $break-md) {
|
||||
.performance__cell {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
@media (min-width: $break-lg) {
|
||||
.updated_at__cell {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-name-cell {
|
||||
.children-wrapper {
|
||||
.query-name-text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
.query-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
|
||||
const baseClass = "automations-modal";
|
||||
|
||||
interface IAutomationsModalProps {
|
||||
onExit: () => void;
|
||||
}
|
||||
|
||||
const AutomationsModal = ({ onExit }: IAutomationsModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal title={"Manage automations"} onExit={onExit} className={baseClass}>
|
||||
<div className={baseClass} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationsModal;
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { omit } from "lodash";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import CustomLink from "components/CustomLink/CustomLink";
|
||||
import Checkbox from "components/forms/fields/Checkbox/Checkbox";
|
||||
import QueryFrequencyIndicator from "components/QueryFrequencyIndicator/QueryFrequencyIndicator";
|
||||
import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator";
|
||||
|
||||
import { ISchedulableQuery } from "interfaces/schedulable_query";
|
||||
|
||||
interface IManageAutomationsModalProps {
|
||||
isUpdatingAutomations: boolean;
|
||||
handleSubmit: (formData: any) => void; // TODO
|
||||
onCancel: () => void;
|
||||
togglePreviewDataModal: () => void;
|
||||
availableQueries?: ISchedulableQuery[];
|
||||
automatedQueryIds: number[];
|
||||
logDestination: string;
|
||||
}
|
||||
|
||||
interface ICheckedQuery {
|
||||
name?: string;
|
||||
id: number;
|
||||
isChecked: boolean;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
const useCheckboxListStateManagement = (
|
||||
allQueries: ISchedulableQuery[],
|
||||
automatedQueryIds: number[] | undefined
|
||||
) => {
|
||||
const [queryItems, setQueryItems] = useState<ICheckedQuery[]>(() => {
|
||||
return allQueries.map(({ name, id, interval }) => ({
|
||||
name,
|
||||
id,
|
||||
isChecked: !!automatedQueryIds?.includes(id),
|
||||
interval,
|
||||
}));
|
||||
});
|
||||
|
||||
const updateQueryItems = (queryId: number) => {
|
||||
setQueryItems((prevItems) =>
|
||||
prevItems.map((query) =>
|
||||
query.id !== queryId ? query : { ...query, isChecked: !query.isChecked }
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return { queryItems, updateQueryItems };
|
||||
};
|
||||
|
||||
const baseClass = "manage-automations-modal";
|
||||
|
||||
const ManageAutomationsModal = ({
|
||||
isUpdatingAutomations,
|
||||
automatedQueryIds,
|
||||
handleSubmit,
|
||||
onCancel,
|
||||
togglePreviewDataModal,
|
||||
availableQueries,
|
||||
logDestination,
|
||||
}: IManageAutomationsModalProps): JSX.Element => {
|
||||
// TODO: Error handling, if any
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
// Client side sort queries alphabetically
|
||||
const sortedAvailableQueries =
|
||||
availableQueries?.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
) || [];
|
||||
|
||||
const { queryItems, updateQueryItems } = useCheckboxListStateManagement(
|
||||
sortedAvailableQueries,
|
||||
automatedQueryIds || []
|
||||
);
|
||||
|
||||
const onSubmit = (evt: React.MouseEvent<HTMLFormElement> | KeyboardEvent) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const newQueryIds: number[] = [];
|
||||
queryItems?.forEach((p) => p.isChecked && newQueryIds.push(p.id));
|
||||
|
||||
handleSubmit(newQueryIds);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: KeyboardEvent) => {
|
||||
if (event.code === "Enter" || event.code === "NumpadEnter") {
|
||||
event.preventDefault();
|
||||
onSubmit(event);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", listener);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", listener);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={"Manage automations"}
|
||||
onExit={onCancel}
|
||||
className={baseClass}
|
||||
width="large"
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__heading`}>
|
||||
Query automations let you send data to your log destination on a
|
||||
schedule. Data is sent according to a query’s frequency.
|
||||
</div>
|
||||
{availableQueries?.length ? (
|
||||
<div className={`${baseClass}__select`}>
|
||||
<p>
|
||||
<strong>Choose which queries will send data:</strong>
|
||||
</p>
|
||||
<div className={`${baseClass}__checkboxes`}>
|
||||
{queryItems &&
|
||||
queryItems.map((queryItem) => {
|
||||
const { isChecked, name, id, interval } = queryItem;
|
||||
return (
|
||||
<div key={id} className={`${baseClass}__query-item`}>
|
||||
<Checkbox
|
||||
value={isChecked}
|
||||
name={name}
|
||||
onChange={() => {
|
||||
updateQueryItems(id);
|
||||
!isChecked &&
|
||||
setErrors((errs) => omit(errs, "queryItems"));
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
<QueryFrequencyIndicator
|
||||
frequency={interval}
|
||||
checked={isChecked}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${baseClass}__no-queries`}>
|
||||
<b>You have no queries.</b>
|
||||
<p>Add a query to turn on automations.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__log-destination`}>
|
||||
<p>
|
||||
<strong>Log destination:</strong>
|
||||
</p>
|
||||
<div className={`${baseClass}__selection`}>
|
||||
<LogDestinationIndicator logDestination={logDestination} />
|
||||
</div>
|
||||
<div className={`${baseClass}__configure`}>
|
||||
Users with the admin role can
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/log-destinations"
|
||||
text="configure a different log destination"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InfoBanner className={`${baseClass}__supported-platforms`}>
|
||||
<p>Automations currently run on macOS, Windows, and Linux hosts.</p>
|
||||
<p>
|
||||
Interested in query automations for your Chromebooks?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/contact"
|
||||
text="Let us know"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<div className={`${baseClass}__preview-btn-wrap`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="inverse"
|
||||
onClick={togglePreviewDataModal}
|
||||
>
|
||||
Preview data
|
||||
</Button>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onSubmit}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingAutomations}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageAutomationsModal;
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
.manage-automations-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-xlarge;
|
||||
|
||||
&__selection {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
|
||||
&__checkboxes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__query-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
}
|
||||
|
||||
&__configure {
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
|
||||
.info-banner {
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fleet-checkbox {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
width: 490px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--checkbox {
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ManageAutomationsModal";
|
||||
|
|
@ -39,11 +39,13 @@ interface IQueriesTableProps {
|
|||
query?: string;
|
||||
order_key?: string;
|
||||
order_direction?: "asc" | "desc";
|
||||
team_id?: string;
|
||||
};
|
||||
isInherited?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_DIRECTION = "desc";
|
||||
const DEFAULT_SORT_HEADER = "updated_at";
|
||||
const DEFAULT_SORT_DIRECTION = "asc";
|
||||
const DEFAULT_SORT_HEADER = "name";
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
const DEFAULT_PLATFORM = "all";
|
||||
|
||||
|
|
@ -88,16 +90,16 @@ const QueriesTable = ({
|
|||
isOnlyObserver,
|
||||
isObserverPlus,
|
||||
isAnyTeamObserverPlus,
|
||||
queryParams,
|
||||
router,
|
||||
queryParams,
|
||||
isInherited = false,
|
||||
}: IQueriesTableProps): JSX.Element | null => {
|
||||
const { currentUser } = useContext(AppContext);
|
||||
|
||||
// Functions to avoid race conditions
|
||||
const initialSearchQuery = (() => queryParams?.query ?? "")();
|
||||
const initialSortHeader = (() =>
|
||||
(queryParams?.order_key as "updated_at" | "name" | "author") ??
|
||||
"updated_at")();
|
||||
(queryParams?.order_key as "name" | "updated_at" | "author") ?? "name")();
|
||||
const initialSortDirection = (() =>
|
||||
(queryParams?.order_direction as "asc" | "desc") ?? "asc")();
|
||||
const initialPlatform = (() =>
|
||||
|
|
@ -143,6 +145,7 @@ const QueriesTable = ({
|
|||
) {
|
||||
newQueryParams.page = 0;
|
||||
}
|
||||
newQueryParams.team_id = queryParams?.team_id;
|
||||
const locationPath = getNextLocationPath({
|
||||
pathPrefix: PATHS.MANAGE_QUERIES,
|
||||
queryParams: newQueryParams,
|
||||
|
|
@ -233,19 +236,21 @@ const QueriesTable = ({
|
|||
};
|
||||
|
||||
const tableHeaders = useMemo(
|
||||
() => currentUser && generateTableHeaders({ currentUser }),
|
||||
[currentUser]
|
||||
() => currentUser && generateTableHeaders({ currentUser, isInherited }),
|
||||
[currentUser, isInherited]
|
||||
);
|
||||
|
||||
const searchable = !(queriesList?.length === 0 && searchQuery === "");
|
||||
const searchable =
|
||||
!(queriesList?.length === 0 && searchQuery === "") && !isInherited;
|
||||
|
||||
return tableHeaders && !isLoading ? (
|
||||
<div className={`${baseClass}`}>
|
||||
<TableContainer
|
||||
disableCount={isInherited}
|
||||
resultsTitle="queries"
|
||||
columns={tableHeaders}
|
||||
data={queriesList}
|
||||
filters={{ global: searchQuery }}
|
||||
filters={{ global: isInherited ? "" : searchQuery }}
|
||||
isLoading={isLoading}
|
||||
defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION}
|
||||
|
|
@ -267,7 +272,9 @@ const QueriesTable = ({
|
|||
isAllPagesSelected={false}
|
||||
searchable={searchable}
|
||||
searchQueryColumn="name"
|
||||
customControl={searchable ? renderPlatformDropdown : undefined}
|
||||
customControl={
|
||||
searchable && !isInherited ? renderPlatformDropdown : undefined
|
||||
}
|
||||
isClientSidePagination
|
||||
onClientSidePaginationChange={onClientSidePaginationChange}
|
||||
isClientSideFilter
|
||||
|
|
@ -278,7 +285,7 @@ const QueriesTable = ({
|
|||
variant: "text-icon",
|
||||
onActionButtonClick: onDeleteQueryClick,
|
||||
}}
|
||||
selectedDropdownFilter={platform}
|
||||
selectedDropdownFilter={!isInherited ? platform : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
|||
import PATHS from "router/paths";
|
||||
|
||||
import permissionsUtils from "utilities/permissions";
|
||||
import { IQuery } from "interfaces/query";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { addGravatarUrlToResource } from "utilities/helpers";
|
||||
import { secondsToDhms } from "utilities/helpers";
|
||||
import {
|
||||
IEnhancedQuery,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { SupportedPlatform } from "interfaces/platform";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import Avatar from "components/Avatar";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
||||
|
|
@ -20,10 +23,12 @@ import PlatformCell from "components/TableContainer/DataTable/PlatformCell";
|
|||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import PillCell from "components/TableContainer/DataTable/PillCell";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator";
|
||||
|
||||
interface IQueryRow {
|
||||
id: string;
|
||||
original: IQuery;
|
||||
original: ISchedulableQuery;
|
||||
}
|
||||
|
||||
interface IGetToggleAllRowsSelectedProps {
|
||||
|
|
@ -46,7 +51,7 @@ interface IHeaderProps {
|
|||
}
|
||||
interface IRowProps {
|
||||
row: {
|
||||
original: IQuery;
|
||||
original: IEnhancedQuery;
|
||||
getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps;
|
||||
toggleRowSelected: () => void;
|
||||
};
|
||||
|
|
@ -55,13 +60,26 @@ interface IRowProps {
|
|||
|
||||
interface ICellProps extends IRowProps {
|
||||
cell: {
|
||||
value: string;
|
||||
value: string | number | boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface INumberCellProps extends IRowProps {
|
||||
cell: {
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface IStringCellProps extends IRowProps {
|
||||
cell: { value: string };
|
||||
}
|
||||
|
||||
interface IBoolCellProps extends IRowProps {
|
||||
cell: { value: boolean };
|
||||
}
|
||||
interface IPlatformCellProps extends IRowProps {
|
||||
cell: {
|
||||
value: string[];
|
||||
value: SupportedPlatform[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +87,10 @@ interface IDataColumn {
|
|||
Header: ((props: IHeaderProps) => JSX.Element) | string;
|
||||
Cell:
|
||||
| ((props: ICellProps) => JSX.Element)
|
||||
| ((props: IPlatformCellProps) => JSX.Element);
|
||||
| ((props: IPlatformCellProps) => JSX.Element)
|
||||
| ((props: IStringCellProps) => JSX.Element)
|
||||
| ((props: INumberCellProps) => JSX.Element)
|
||||
| ((props: IBoolCellProps) => JSX.Element);
|
||||
id?: string;
|
||||
title?: string;
|
||||
accessor?: string;
|
||||
|
|
@ -80,17 +101,16 @@ interface IDataColumn {
|
|||
|
||||
interface IGenerateTableHeaders {
|
||||
currentUser: IUser;
|
||||
isInherited?: boolean;
|
||||
}
|
||||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
const generateTableHeaders = ({
|
||||
currentUser,
|
||||
isInherited = false,
|
||||
}: IGenerateTableHeaders): IDataColumn[] => {
|
||||
const isOnlyObserver = permissionsUtils.isOnlyObserver(currentUser);
|
||||
const isAnyTeamMaintainerOrTeamAdmin = permissionsUtils.isAnyTeamMaintainerOrTeamAdmin(
|
||||
currentUser
|
||||
);
|
||||
|
||||
const tableHeaders: IDataColumn[] = [
|
||||
{
|
||||
|
|
@ -105,7 +125,7 @@ const generateTableHeaders = ({
|
|||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
return (
|
||||
<LinkCell
|
||||
classes="w400"
|
||||
classes="w400 query-name-cell"
|
||||
value={
|
||||
<>
|
||||
<div className="query-name-text">{cellProps.cell.value}</div>
|
||||
|
|
@ -124,7 +144,7 @@ const generateTableHeaders = ({
|
|||
type="dark"
|
||||
effect="solid"
|
||||
id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
|
||||
backgroundColor="#3e4771"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
Observers can run this query.
|
||||
</ReactTooltip>
|
||||
|
|
@ -132,7 +152,10 @@ const generateTableHeaders = ({
|
|||
)}
|
||||
</>
|
||||
}
|
||||
path={PATHS.EDIT_QUERY(cellProps.row.original)}
|
||||
path={PATHS.EDIT_QUERY(
|
||||
cellProps.row.original.id,
|
||||
cellProps.row.original.team_id ?? undefined
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
@ -140,38 +163,37 @@ const generateTableHeaders = ({
|
|||
},
|
||||
{
|
||||
title: "Platform",
|
||||
Header: "Platform",
|
||||
Header: "Compatible with",
|
||||
disableSortBy: true,
|
||||
accessor: "platforms",
|
||||
Cell: (cellProps: IPlatformCellProps): JSX.Element => {
|
||||
return <PlatformCell value={cellProps.cell.value} />;
|
||||
return <PlatformCell platforms={cellProps.row.original.platforms} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Author",
|
||||
Header: (cellProps) => (
|
||||
<HeaderCell
|
||||
value={cellProps.column.title}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
),
|
||||
accessor: "author_name",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
const { author_name, author_email } = cellProps.row.original;
|
||||
const author = author_name === currentUser.name ? "You" : author_name;
|
||||
title: "Frequency",
|
||||
Header: "Frequency",
|
||||
disableSortBy: true,
|
||||
accessor: "interval",
|
||||
Cell: (cellProps: INumberCellProps): JSX.Element => {
|
||||
const val = cellProps.cell.value
|
||||
? `Every ${secondsToDhms(cellProps.cell.value)}`
|
||||
: undefined;
|
||||
return (
|
||||
<span>
|
||||
<Avatar
|
||||
user={addGravatarUrlToResource({ email: author_email })}
|
||||
size="xsmall"
|
||||
/>
|
||||
<span className="text-cell author-name">{author}</span>
|
||||
</span>
|
||||
<TextCell
|
||||
value={val}
|
||||
emptyCellTooltipText={
|
||||
<>
|
||||
Assign a frequency and turn <strong>automations</strong> on to
|
||||
collect data at an interval.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortType: "caseInsensitive",
|
||||
},
|
||||
{
|
||||
title: "Performance impact",
|
||||
Header: () => {
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -189,7 +211,7 @@ const generateTableHeaders = ({
|
|||
},
|
||||
disableSortBy: true,
|
||||
accessor: "performance",
|
||||
Cell: (cellProps: ICellProps) => (
|
||||
Cell: (cellProps: IStringCellProps) => (
|
||||
<PillCell
|
||||
value={{
|
||||
indicator: cellProps.cell.value,
|
||||
|
|
@ -198,6 +220,20 @@ const generateTableHeaders = ({
|
|||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Automations",
|
||||
Header: "Automations",
|
||||
disableSortBy: true,
|
||||
accessor: "automations_enabled",
|
||||
Cell: (cellProps: IBoolCellProps): JSX.Element => {
|
||||
return (
|
||||
<QueryAutomationsStatusIndicator
|
||||
automationsEnabled={cellProps.cell.value}
|
||||
interval={cellProps.row.original.interval}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Last modified",
|
||||
Header: (cellProps) => (
|
||||
|
|
@ -207,7 +243,7 @@ const generateTableHeaders = ({
|
|||
/>
|
||||
),
|
||||
accessor: "updated_at",
|
||||
Cell: (cellProps: ICellProps): JSX.Element => (
|
||||
Cell: (cellProps: INumberCellProps): JSX.Element => (
|
||||
<TextCell
|
||||
value={formatDistanceToNow(new Date(cellProps.cell.value), {
|
||||
includeSeconds: true,
|
||||
|
|
@ -217,62 +253,21 @@ const generateTableHeaders = ({
|
|||
),
|
||||
},
|
||||
];
|
||||
if (!isOnlyObserver) {
|
||||
if (!isOnlyObserver && !isInherited) {
|
||||
tableHeaders.splice(0, 0, {
|
||||
id: "selection",
|
||||
Header: (cellProps: IHeaderProps): JSX.Element => {
|
||||
const {
|
||||
getToggleAllRowsSelectedProps,
|
||||
rows,
|
||||
selectedFlatRows,
|
||||
toggleAllRowsSelected,
|
||||
toggleRowSelected,
|
||||
} = cellProps;
|
||||
const { checked, indeterminate } = getToggleAllRowsSelectedProps();
|
||||
|
||||
const disableToggleAllRowsSelected = () => {
|
||||
/* Team admin or team maintainer can only delete queries they authored
|
||||
If team admin or team maintainer authored 0 queries, disable select all queries for deletion */
|
||||
if (isAnyTeamMaintainerOrTeamAdmin) {
|
||||
return (
|
||||
rows.filter(
|
||||
(r: IQueryRow) => r.original.author_id === currentUser.id
|
||||
).length === 0
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const checkboxProps = {
|
||||
value: checked,
|
||||
indeterminate,
|
||||
disabled: disableToggleAllRowsSelected(), // Disable select all if all rows are disabled
|
||||
onChange: () => {
|
||||
if (!isAnyTeamMaintainerOrTeamAdmin) {
|
||||
toggleAllRowsSelected();
|
||||
} else {
|
||||
// Team maintainers may only delete the queries that they have authored
|
||||
// so we need to do some filtering and then modify the toggle select all
|
||||
// behavior for the header checkbox
|
||||
const userAuthoredQueries = rows.filter(
|
||||
(r: IQueryRow) => r.original.author_id === currentUser.id
|
||||
);
|
||||
if (
|
||||
selectedFlatRows.length &&
|
||||
selectedFlatRows.length !== userAuthoredQueries.length
|
||||
) {
|
||||
// If some but not all of the user authored queries are already selected,
|
||||
// we toggle all of the user's unselected queries to true
|
||||
userAuthoredQueries.forEach((r: IQueryRow) =>
|
||||
toggleRowSelected(r.id, true)
|
||||
);
|
||||
} else {
|
||||
// Otherwise, we toggle all of the user's queries to the opposite of their current state
|
||||
userAuthoredQueries.forEach((r: IQueryRow) =>
|
||||
toggleRowSelected(r.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
toggleAllRowsSelected();
|
||||
},
|
||||
};
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
|
|
@ -283,44 +278,9 @@ const generateTableHeaders = ({
|
|||
const checkboxProps = {
|
||||
value: checked,
|
||||
onChange: () => row.toggleRowSelected(),
|
||||
disabled:
|
||||
isAnyTeamMaintainerOrTeamAdmin &&
|
||||
row.original.author_id !== currentUser.id,
|
||||
};
|
||||
// If the user is a team maintainer, we only enable checkboxes for queries
|
||||
// that they authored and we include a tooltip to explain disabled checkboxes
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-tip
|
||||
data-for={`${"select-checkbox"}__${row.original.id}`}
|
||||
data-tip-disable={
|
||||
!isAnyTeamMaintainerOrTeamAdmin ||
|
||||
row.original.author_id === currentUser.id
|
||||
}
|
||||
className={`${
|
||||
!(
|
||||
!isAnyTeamMaintainerOrTeamAdmin ||
|
||||
row.original.author_id === currentUser.id
|
||||
) && "tooltip"
|
||||
}`}
|
||||
>
|
||||
<Checkbox {...checkboxProps} />
|
||||
</div>{" "}
|
||||
<ReactTooltip
|
||||
className="select-checkbox-tooltip"
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
id={`${"select-checkbox"}__${row.original.id}`}
|
||||
data-html
|
||||
>
|
||||
<>
|
||||
You can only delete a<br /> query if you are the author.
|
||||
</>
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
// v4.35.0 Any team admin or maintainer now can add, edit, delete their team's queries
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
},
|
||||
disableHidden: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import StatusIndicator from "components/StatusIndicator";
|
||||
import React from "react";
|
||||
|
||||
interface IQueryAutomationsStatusIndicator {
|
||||
automationsEnabled: boolean;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
const QueryAutomationsStatusIndicator = ({
|
||||
automationsEnabled,
|
||||
interval,
|
||||
}: IQueryAutomationsStatusIndicator) => {
|
||||
let status;
|
||||
if (automationsEnabled) {
|
||||
if (interval === 0) {
|
||||
status = "paused";
|
||||
} else {
|
||||
status = "on";
|
||||
}
|
||||
} else {
|
||||
status = "off";
|
||||
}
|
||||
|
||||
const tooltip =
|
||||
status === "paused"
|
||||
? {
|
||||
tooltipText: (
|
||||
<>
|
||||
<strong>Automations</strong> will resume for this query when a
|
||||
frequency is set.
|
||||
</>
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
return (
|
||||
<StatusIndicator
|
||||
value={status}
|
||||
tooltip={tooltip}
|
||||
customIndicatorType={"query-automations"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryAutomationsStatusIndicator;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.status-indicator {
|
||||
// Query automations status
|
||||
&--query-automations-on {
|
||||
&:before {
|
||||
background-color: $ui-success;
|
||||
}
|
||||
}
|
||||
&--query-automations-off {
|
||||
&:before {
|
||||
background-color: $ui-offline;
|
||||
}
|
||||
}
|
||||
&--query-automations-paused {
|
||||
.status-tooltip {
|
||||
text-transform: none;
|
||||
}
|
||||
&:before {
|
||||
background-color: $ui-offline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./QueryAutomationsStatusIndicator";
|
||||
|
|
@ -12,7 +12,10 @@ import statusAPI from "services/entities/status";
|
|||
import { IHost, IHostResponse } from "interfaces/host";
|
||||
import { ILabel } from "interfaces/label";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IQueryFormData, IQuery, IStoredQueryResponse } from "interfaces/query";
|
||||
import {
|
||||
IGetQueryResponse,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { ITarget } from "interfaces/target";
|
||||
|
||||
import QuerySidePanel from "components/side_panels/QuerySidePanel";
|
||||
|
|
@ -23,12 +26,15 @@ 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: {
|
||||
query: { host_ids: string };
|
||||
pathname: string;
|
||||
query: { host_ids: string; team_id?: string };
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -37,9 +43,18 @@ const baseClass = "query-page";
|
|||
const QueryPage = ({
|
||||
router,
|
||||
params: { id: paramsQueryId },
|
||||
location: { query: URLQuerySearch },
|
||||
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 {
|
||||
|
|
@ -57,6 +72,10 @@ const QueryPage = ({
|
|||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryLoggingType,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryPlatforms,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
|
||||
|
|
@ -78,19 +97,23 @@ const QueryPage = ({
|
|||
isLoading: isStoredQueryLoading,
|
||||
data: storedQuery,
|
||||
error: storedQueryError,
|
||||
} = useQuery<IStoredQueryResponse, Error, IQuery>(
|
||||
} = useQuery<IGetQueryResponse, Error, ISchedulableQuery>(
|
||||
["query", queryId],
|
||||
() => queryAPI.load(queryId as number),
|
||||
{
|
||||
enabled: !!queryId,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data: IStoredQueryResponse) => data.query,
|
||||
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),
|
||||
}
|
||||
|
|
@ -99,9 +122,9 @@ const QueryPage = ({
|
|||
useQuery<IHostResponse, Error, IHost>(
|
||||
"hostFromURL",
|
||||
() =>
|
||||
hostAPI.loadHostDetails(parseInt(URLQuerySearch.host_ids as string, 10)),
|
||||
hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
|
||||
{
|
||||
enabled: !!URLQuerySearch.host_ids && !queryParamHostsAdded,
|
||||
enabled: !!location.query.host_ids && !queryParamHostsAdded,
|
||||
select: (data: IHostResponse) => data.host,
|
||||
onSuccess: (host) => {
|
||||
setTargetedHosts((prevHosts) =>
|
||||
|
|
@ -119,10 +142,6 @@ const QueryPage = ({
|
|||
}
|
||||
);
|
||||
|
||||
const { mutateAsync: createQuery } = useMutation((formData: IQueryFormData) =>
|
||||
queryAPI.create(formData)
|
||||
);
|
||||
|
||||
const detectIsFleetQueryRunnable = () => {
|
||||
statusAPI.live_query().catch(() => {
|
||||
setIsLiveQueryRunnable(false);
|
||||
|
|
@ -131,12 +150,18 @@ const QueryPage = ({
|
|||
|
||||
useEffect(() => {
|
||||
detectIsFleetQueryRunnable();
|
||||
setLastEditedQueryId(DEFAULT_QUERY.id);
|
||||
setLastEditedQueryName(DEFAULT_QUERY.name);
|
||||
setLastEditedQueryDescription(DEFAULT_QUERY.description);
|
||||
setLastEditedQueryBody(DEFAULT_QUERY.query);
|
||||
setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
|
||||
}, []);
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowOpenSchemaActionText(!isSidebarOpen);
|
||||
|
|
@ -179,22 +204,23 @@ const QueryPage = ({
|
|||
const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []);
|
||||
|
||||
const renderScreen = () => {
|
||||
const step1Opts = {
|
||||
const step1Props = {
|
||||
router,
|
||||
baseClass,
|
||||
queryIdForEdit: queryId,
|
||||
teamNameForQuery,
|
||||
apiTeamIdForQuery,
|
||||
showOpenSchemaActionText,
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
storedQueryError,
|
||||
createQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
};
|
||||
|
||||
const step2Opts = {
|
||||
const step2Props = {
|
||||
baseClass,
|
||||
queryId,
|
||||
selectedTargets,
|
||||
|
|
@ -211,7 +237,7 @@ const QueryPage = ({
|
|||
setTargetsTotalCount,
|
||||
};
|
||||
|
||||
const step3Opts = {
|
||||
const step3Props = {
|
||||
queryId,
|
||||
selectedTargets,
|
||||
storedQuery,
|
||||
|
|
@ -222,11 +248,11 @@ const QueryPage = ({
|
|||
|
||||
switch (step) {
|
||||
case QUERIES_PAGE_STEPS[2]:
|
||||
return <SelectTargets {...step2Opts} />;
|
||||
return <SelectTargets {...step2Props} />;
|
||||
case QUERIES_PAGE_STEPS[3]:
|
||||
return <RunQuery {...step3Opts} />;
|
||||
return <RunQuery {...step3Props} />;
|
||||
default:
|
||||
return <QueryEditor {...step1Opts} />;
|
||||
return <QueryEditor {...step1Props} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { size } from "lodash";
|
||||
|
||||
import { IQueryFormData } from "interfaces/query";
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
export interface INewQueryModalProps {
|
||||
baseClass: string;
|
||||
queryValue: string;
|
||||
isLoading: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
setIsSaveModalOpen: (isOpen: boolean) => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
}
|
||||
|
||||
const validateQueryName = (name: string) => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
if (!name) {
|
||||
errors.name = "Query name must be present";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
const NewQueryModal = ({
|
||||
baseClass,
|
||||
queryValue,
|
||||
isLoading,
|
||||
onCreateQuery,
|
||||
setIsSaveModalOpen,
|
||||
backendValidators,
|
||||
}: INewQueryModalProps): JSX.Element => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [observerCanRun, setObserverCanRun] = useState(false);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (name) {
|
||||
setErrors({});
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(backendValidators);
|
||||
}, [backendValidators]);
|
||||
|
||||
const handleUpdate = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validateQueryName(name);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
onCreateQuery({
|
||||
description,
|
||||
name,
|
||||
query: queryValue,
|
||||
observer_can_run: observerCanRun,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={() => setIsSaveModalOpen(false)}>
|
||||
<>
|
||||
<form
|
||||
onSubmit={handleUpdate}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__query-save-modal-name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
autofocus
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__query-save-modal-description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={`${baseClass}__query-save-modal-observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
Users with the Observer role will be able to run this query on hosts
|
||||
where they have access.
|
||||
</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="save-query-loading"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save query
|
||||
</Button>
|
||||
<Button onClick={() => setIsSaveModalOpen(false)} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewQueryModal;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./NewQueryModal";
|
||||
|
|
@ -1,17 +1,35 @@
|
|||
import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { size } from "lodash";
|
||||
import { pull, size } from "lodash";
|
||||
import classnames from "classnames";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { addGravatarUrlToResource } from "utilities/helpers";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
|
||||
MIN_OSQUERY_VERSION_OPTIONS,
|
||||
LOGGING_TYPE_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
import usePlatformCompatibility from "hooks/usePlatformCompatibility";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { IQuery, IQueryFormData } from "interfaces/query";
|
||||
import {
|
||||
ISchedulableQuery,
|
||||
ICreateQueryRequestBody,
|
||||
QueryLoggingOption,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
import queryAPI from "services/entities/queries";
|
||||
|
||||
import { IAceEditor } from "react-ace/lib/types";
|
||||
|
|
@ -23,10 +41,12 @@ import validateQuery from "components/forms/validators/validate_query";
|
|||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import NewQueryModal from "../NewQueryModal";
|
||||
import SaveQueryModal from "../SaveQueryModal";
|
||||
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
|
||||
|
||||
const baseClass = "query-form";
|
||||
|
|
@ -34,15 +54,17 @@ const baseClass = "query-form";
|
|||
interface IQueryFormProps {
|
||||
router: InjectedRouter;
|
||||
queryIdForEdit: number | null;
|
||||
apiTeamIdForQuery?: number;
|
||||
teamNameForQuery?: string;
|
||||
showOpenSchemaActionText: boolean;
|
||||
storedQuery: IQuery | undefined;
|
||||
storedQuery: ISchedulableQuery | undefined;
|
||||
isStoredQueryLoading: boolean;
|
||||
isQuerySaving: boolean;
|
||||
isQueryUpdating: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
saveQuery: (formData: ICreateQueryRequestBody) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
onUpdate: (formData: IQueryFormData) => void;
|
||||
onUpdate: (formData: ICreateQueryRequestBody) => void;
|
||||
onOpenSchemaSidebar: () => void;
|
||||
renderLiveQueryWarning: () => JSX.Element | null;
|
||||
backendValidators: { [key: string]: string };
|
||||
|
|
@ -63,12 +85,14 @@ const validateQuerySQL = (query: string) => {
|
|||
const QueryForm = ({
|
||||
router,
|
||||
queryIdForEdit,
|
||||
apiTeamIdForQuery,
|
||||
teamNameForQuery,
|
||||
showOpenSchemaActionText,
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
isQuerySaving,
|
||||
isQueryUpdating,
|
||||
onCreateQuery,
|
||||
saveQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
onUpdate,
|
||||
|
|
@ -84,10 +108,18 @@ const QueryForm = ({
|
|||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
lastEditedQueryLoggingType,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryObserverCanRun,
|
||||
setLastEditedQueryFrequency,
|
||||
setLastEditedQueryPlatforms,
|
||||
setLastEditedQueryMinOsqueryVersion,
|
||||
setLastEditedQueryLoggingType,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const {
|
||||
|
|
@ -104,13 +136,14 @@ const QueryForm = ({
|
|||
|
||||
const savedQueryMode = !!queryIdForEdit;
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
|
||||
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
||||
const [showSaveQueryModal, setShowSaveQueryModal] = useState(false);
|
||||
const [showQueryEditor, setShowQueryEditor] = useState(
|
||||
isObserverPlus || isAnyTeamObserverPlus || false
|
||||
);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||
const [isSaveAsNewLoading, setIsSaveAsNewLoading] = useState(false);
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const platformCompatibility = usePlatformCompatibility();
|
||||
const { setCompatiblePlatforms } = platformCompatibility;
|
||||
|
|
@ -133,7 +166,7 @@ const QueryForm = ({
|
|||
}
|
||||
|
||||
debounceSQL(lastEditedQueryBody);
|
||||
}, [lastEditedQueryBody, lastEditedQueryId]);
|
||||
}, [lastEditedQueryBody, lastEditedQueryId, isStoredQueryLoading]);
|
||||
|
||||
const hasTeamMaintainerPermissions = savedQueryMode
|
||||
? isAnyTeamMaintainerOrTeamAdmin &&
|
||||
|
|
@ -142,6 +175,10 @@ const QueryForm = ({
|
|||
storedQuery.author_id === currentUser.id
|
||||
: isAnyTeamMaintainerOrTeamAdmin;
|
||||
|
||||
const toggleSaveQueryModal = () => {
|
||||
setShowSaveQueryModal(!showSaveQueryModal);
|
||||
};
|
||||
|
||||
const onLoad = (editor: IAceEditor) => {
|
||||
editor.setOptions({
|
||||
enableLinking: true,
|
||||
|
|
@ -173,6 +210,50 @@ const QueryForm = ({
|
|||
}
|
||||
};
|
||||
|
||||
const onChangeSelectFrequency = useCallback(
|
||||
(value: number) => {
|
||||
setLastEditedQueryFrequency(value);
|
||||
},
|
||||
[setLastEditedQueryFrequency]
|
||||
);
|
||||
|
||||
const toggleAdvancedOptions = () => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
};
|
||||
|
||||
const onChangeSelectPlatformOptions = useCallback(
|
||||
(values: string) => {
|
||||
const valArray = values.split(",");
|
||||
|
||||
// Remove All if another OS is chosen
|
||||
// else if Remove OS if All is chosen
|
||||
if (valArray.indexOf("") === 0 && valArray.length > 1) {
|
||||
setLastEditedQueryPlatforms(
|
||||
pull(valArray, "").join(",") as SelectedPlatformString
|
||||
);
|
||||
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
|
||||
setLastEditedQueryPlatforms("");
|
||||
} else {
|
||||
setLastEditedQueryPlatforms(values as SelectedPlatformString);
|
||||
}
|
||||
},
|
||||
[setLastEditedQueryPlatforms]
|
||||
);
|
||||
|
||||
const onChangeMinOsqueryVersionOptions = useCallback(
|
||||
(value: string) => {
|
||||
setLastEditedQueryMinOsqueryVersion(value);
|
||||
},
|
||||
[setLastEditedQueryMinOsqueryVersion]
|
||||
);
|
||||
|
||||
const onChangeSelectLoggingType = useCallback(
|
||||
(value: QueryLoggingOption) => {
|
||||
setLastEditedQueryLoggingType(value);
|
||||
},
|
||||
[setLastEditedQueryLoggingType]
|
||||
);
|
||||
|
||||
const promptSaveAsNewQuery = () => (
|
||||
evt: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
|
|
@ -192,17 +273,21 @@ const QueryForm = ({
|
|||
|
||||
if (valid) {
|
||||
setIsSaveAsNewLoading(true);
|
||||
|
||||
queryAPI
|
||||
.create({
|
||||
name: lastEditedQueryName,
|
||||
description: lastEditedQueryDescription,
|
||||
query: lastEditedQueryBody,
|
||||
team_id: apiTeamIdForQuery,
|
||||
observer_can_run: lastEditedQueryObserverCanRun,
|
||||
interval: lastEditedQueryFrequency,
|
||||
platform: lastEditedQueryPlatforms,
|
||||
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
||||
logging: lastEditedQueryLoggingType,
|
||||
})
|
||||
.then((response: { query: IQuery }) => {
|
||||
.then((response: { query: ISchedulableQuery }) => {
|
||||
setIsSaveAsNewLoading(false);
|
||||
router.push(PATHS.EDIT_QUERY(response.query));
|
||||
router.push(PATHS.EDIT_QUERY(response.query.id));
|
||||
renderFlash("success", `Successfully added query.`);
|
||||
})
|
||||
.catch((createError: { data: IApiError }) => {
|
||||
|
|
@ -212,11 +297,16 @@ const QueryForm = ({
|
|||
name: `Copy of ${lastEditedQueryName}`,
|
||||
description: lastEditedQueryDescription,
|
||||
query: lastEditedQueryBody,
|
||||
team_id: apiTeamIdForQuery,
|
||||
observer_can_run: lastEditedQueryObserverCanRun,
|
||||
interval: lastEditedQueryFrequency,
|
||||
platform: lastEditedQueryPlatforms,
|
||||
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
||||
logging: lastEditedQueryLoggingType,
|
||||
})
|
||||
.then((response: { query: IQuery }) => {
|
||||
.then((response: { query: ISchedulableQuery }) => {
|
||||
setIsSaveAsNewLoading(false);
|
||||
router.push(PATHS.EDIT_QUERY(response.query));
|
||||
router.push(PATHS.EDIT_QUERY(response.query.id));
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully added query as "Copy of ${lastEditedQueryName}".`
|
||||
|
|
@ -228,9 +318,19 @@ const QueryForm = ({
|
|||
"already exists"
|
||||
)
|
||||
) {
|
||||
let teamErrorText;
|
||||
if (apiTeamIdForQuery !== 0) {
|
||||
if (teamNameForQuery) {
|
||||
teamErrorText = `the ${teamNameForQuery} team`;
|
||||
} else {
|
||||
teamErrorText = "this team";
|
||||
}
|
||||
} else {
|
||||
teamErrorText = "all teams";
|
||||
}
|
||||
renderFlash(
|
||||
"error",
|
||||
`"Copy of ${lastEditedQueryName}" already exists. Please rename your query and try again.`
|
||||
`A query called "Copy of ${lastEditedQueryName}" already exists for ${teamErrorText}.`
|
||||
);
|
||||
}
|
||||
setIsSaveAsNewLoading(false);
|
||||
|
|
@ -260,13 +360,17 @@ const QueryForm = ({
|
|||
|
||||
if (valid) {
|
||||
if (!savedQueryMode) {
|
||||
setIsSaveModalOpen(true);
|
||||
setShowSaveQueryModal(true);
|
||||
} else {
|
||||
onUpdate({
|
||||
name: lastEditedQueryName,
|
||||
description: lastEditedQueryDescription,
|
||||
query: lastEditedQueryBody,
|
||||
observer_can_run: lastEditedQueryObserverCanRun,
|
||||
interval: lastEditedQueryFrequency,
|
||||
platform: lastEditedQueryPlatforms,
|
||||
min_osquery_version: lastEditedQueryMinOsqueryVersion,
|
||||
logging: lastEditedQueryLoggingType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -445,7 +549,7 @@ const QueryForm = ({
|
|||
variant="blue-green"
|
||||
onClick={goToSelectTargets}
|
||||
>
|
||||
Run query
|
||||
Live query
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -482,21 +586,72 @@ const QueryForm = ({
|
|||
{renderPlatformCompatibility()}
|
||||
</span>
|
||||
{savedQueryMode && (
|
||||
<>
|
||||
<Checkbox
|
||||
value={lastEditedQueryObserverCanRun}
|
||||
onChange={(value: boolean) =>
|
||||
setLastEditedQueryObserverCanRun(value)
|
||||
}
|
||||
wrapperClassName={`${baseClass}__query-observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
Users with the observer role will be able to run this query on
|
||||
hosts where they have access.
|
||||
</p>
|
||||
</>
|
||||
<div className={`${baseClass}__edit-options`}>
|
||||
<div className={`${baseClass}__frequency`}>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={onChangeSelectFrequency}
|
||||
placeholder={"Every day"}
|
||||
value={lastEditedQueryFrequency}
|
||||
label={"Frequency"}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
/>
|
||||
If automations are on, this is how often your query collects data.
|
||||
</div>
|
||||
<div className={`${baseClass}__observers-can-run`}>
|
||||
<Checkbox
|
||||
value={lastEditedQueryObserverCanRun}
|
||||
onChange={(value: boolean) =>
|
||||
setLastEditedQueryObserverCanRun(value)
|
||||
}
|
||||
wrapperClassName={`${baseClass}__query-observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
Users with the observer role will be able to run this query on
|
||||
hosts where they have access.
|
||||
</p>
|
||||
</div>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className={baseClass}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div className={`${baseClass}__advanced-options`}>
|
||||
<Dropdown
|
||||
options={SCHEDULE_PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platform"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={lastEditedQueryPlatforms}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={onChangeMinOsqueryVersionOptions}
|
||||
placeholder="Select"
|
||||
value={lastEditedQueryMinOsqueryVersion}
|
||||
label="Minimum osquery version"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={LOGGING_TYPE_OPTIONS}
|
||||
onChange={onChangeSelectLoggingType}
|
||||
placeholder="Select"
|
||||
value={lastEditedQueryLoggingType}
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderLiveQueryWarning()}
|
||||
<div
|
||||
|
|
@ -519,10 +674,12 @@ const QueryForm = ({
|
|||
<div
|
||||
data-tip
|
||||
data-for="save-query-button"
|
||||
// Tooltip shows for team maintainer/admins viewing global queries
|
||||
data-tip-disable={
|
||||
!(
|
||||
isAnyTeamMaintainerOrTeamAdmin &&
|
||||
!hasTeamMaintainerPermissions
|
||||
!storedQuery?.team_id &&
|
||||
!!queryIdForEdit
|
||||
)
|
||||
}
|
||||
>
|
||||
|
|
@ -530,9 +687,11 @@ const QueryForm = ({
|
|||
className="save-loading"
|
||||
variant="brand"
|
||||
onClick={promptSaveQuery()}
|
||||
// Button disabled for team maintainer/admins viewing global queries
|
||||
disabled={
|
||||
isAnyTeamMaintainerOrTeamAdmin &&
|
||||
!hasTeamMaintainerPermissions
|
||||
!storedQuery?.team_id &&
|
||||
!!queryIdForEdit
|
||||
}
|
||||
isLoading={isQueryUpdating}
|
||||
>
|
||||
|
|
@ -541,16 +700,15 @@ const QueryForm = ({
|
|||
</div>{" "}
|
||||
<ReactTooltip
|
||||
className={`save-query-button-tooltip`}
|
||||
place="bottom"
|
||||
place="top"
|
||||
effect="solid"
|
||||
backgroundColor="#3e4771"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
id="save-query-button"
|
||||
data-html
|
||||
>
|
||||
<>
|
||||
You can only save
|
||||
<br /> changes to a query if you
|
||||
<br /> are the author.
|
||||
You can only save changes
|
||||
<br /> to a team level query.
|
||||
</>
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
|
|
@ -561,16 +719,16 @@ const QueryForm = ({
|
|||
variant="blue-green"
|
||||
onClick={goToSelectTargets}
|
||||
>
|
||||
Run query
|
||||
Live query
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isSaveModalOpen && (
|
||||
<NewQueryModal
|
||||
baseClass={baseClass}
|
||||
{showSaveQueryModal && (
|
||||
<SaveQueryModal
|
||||
queryValue={lastEditedQueryBody}
|
||||
onCreateQuery={onCreateQuery}
|
||||
setIsSaveModalOpen={setIsSaveModalOpen}
|
||||
apiTeamIdForQuery={apiTeamIdForQuery}
|
||||
saveQuery={saveQuery}
|
||||
toggleSaveQueryModal={toggleSaveQueryModal}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={isQuerySaving}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,22 @@
|
|||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__edit-options {
|
||||
> div:not(:last-child) {
|
||||
margin-bottom: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
&__frequency {
|
||||
.form-field {
|
||||
margin-bottom: $pad-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__advanced-options {
|
||||
margin-top: $pad-medium;
|
||||
}
|
||||
|
||||
&__query-observer-can-run-wrapper {
|
||||
margin: 0;
|
||||
margin-top: $pad-large;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { pull, size } from "lodash";
|
||||
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
LOGGING_TYPE_OPTIONS,
|
||||
MIN_OSQUERY_VERSION_OPTIONS,
|
||||
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import { SelectedPlatformString } from "interfaces/platform";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
ISchedulableQuery,
|
||||
QueryLoggingOption,
|
||||
} from "interfaces/schedulable_query";
|
||||
|
||||
const baseClass = "save-query-modal";
|
||||
export interface ISaveQueryModalProps {
|
||||
queryValue: string;
|
||||
apiTeamIdForQuery?: number; // query will be global if omitted
|
||||
isLoading: boolean;
|
||||
saveQuery: (formData: ICreateQueryRequestBody) => void;
|
||||
toggleSaveQueryModal: () => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
existingQuery?: ISchedulableQuery;
|
||||
}
|
||||
|
||||
const validateQueryName = (name: string) => {
|
||||
const errors: { [key: string]: string } = {};
|
||||
|
||||
if (!name) {
|
||||
errors.name = "Query name must be present";
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
const SaveQueryModal = ({
|
||||
queryValue,
|
||||
apiTeamIdForQuery,
|
||||
isLoading,
|
||||
saveQuery,
|
||||
toggleSaveQueryModal,
|
||||
backendValidators,
|
||||
existingQuery,
|
||||
}: ISaveQueryModalProps): JSX.Element => {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [selectedFrequency, setSelectedFrequency] = useState(
|
||||
existingQuery?.interval ?? 3600
|
||||
);
|
||||
const [
|
||||
selectedPlatformOptions,
|
||||
setSelectedPlatformOptions,
|
||||
] = useState<SelectedPlatformString>(existingQuery?.platform ?? "");
|
||||
const [
|
||||
selectedMinOsqueryVersionOptions,
|
||||
setSelectedMinOsqueryVersionOptions,
|
||||
] = useState(existingQuery?.min_osquery_version ?? "");
|
||||
const [
|
||||
selectedLoggingType,
|
||||
setSelectedLoggingType,
|
||||
] = useState<QueryLoggingOption>(existingQuery?.logging ?? "snapshot");
|
||||
const [observerCanRun, setObserverCanRun] = useState(false);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const toggleAdvancedOptions = () => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
};
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (name) {
|
||||
setErrors({});
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(backendValidators);
|
||||
}, [backendValidators]);
|
||||
|
||||
const onClickSaveQuery = (evt: React.MouseEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { valid, errors: newErrors } = validateQueryName(name);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
});
|
||||
|
||||
if (valid) {
|
||||
saveQuery({
|
||||
// from modal fields
|
||||
name,
|
||||
description,
|
||||
interval: selectedFrequency,
|
||||
observer_can_run: observerCanRun,
|
||||
platform: selectedPlatformOptions,
|
||||
min_osquery_version: selectedMinOsqueryVersionOptions,
|
||||
logging: selectedLoggingType,
|
||||
// from previous New query page
|
||||
query: queryValue,
|
||||
// from doubly previous ManageQueriesPage
|
||||
team_id: apiTeamIdForQuery,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSelectPlatformOptions = useCallback(
|
||||
(values: string) => {
|
||||
const valArray = values.split(",");
|
||||
|
||||
// Remove All if another OS is chosen
|
||||
// else if Remove OS if All is chosen
|
||||
if (valArray.indexOf("") === 0 && valArray.length > 1) {
|
||||
// TODO - inmprove type safety of all 3 options
|
||||
setSelectedPlatformOptions(
|
||||
pull(valArray, "").join(",") as SelectedPlatformString
|
||||
);
|
||||
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
|
||||
setSelectedPlatformOptions("");
|
||||
} else {
|
||||
setSelectedPlatformOptions(values as SelectedPlatformString);
|
||||
}
|
||||
},
|
||||
[setSelectedPlatformOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={toggleSaveQueryModal}>
|
||||
<>
|
||||
<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
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveQueryModal;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SaveQueryModal";
|
||||
|
|
@ -7,7 +7,10 @@ import queryAPI from "services/entities/queries";
|
|||
import { AppContext } from "context/app";
|
||||
import { QueryContext } from "context/query";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { IQueryFormData, IQuery } from "interfaces/query";
|
||||
import {
|
||||
ICreateQueryRequestBody,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import PATHS from "router/paths";
|
||||
import debounce from "utilities/debounce";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
|
|
@ -19,16 +22,12 @@ interface IQueryEditorProps {
|
|||
router: InjectedRouter;
|
||||
baseClass: string;
|
||||
queryIdForEdit: number | null;
|
||||
storedQuery: IQuery | undefined;
|
||||
teamNameForQuery?: string;
|
||||
apiTeamIdForQuery?: number;
|
||||
storedQuery: ISchedulableQuery | undefined;
|
||||
storedQueryError: Error | null;
|
||||
showOpenSchemaActionText: boolean;
|
||||
isStoredQueryLoading: boolean;
|
||||
createQuery: UseMutateAsyncFunction<
|
||||
{ query: IQuery },
|
||||
unknown,
|
||||
IQueryFormData,
|
||||
unknown
|
||||
>;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
onOpenSchemaSidebar: () => void;
|
||||
|
|
@ -39,11 +38,12 @@ const QueryEditor = ({
|
|||
router,
|
||||
baseClass,
|
||||
queryIdForEdit,
|
||||
teamNameForQuery,
|
||||
apiTeamIdForQuery,
|
||||
storedQuery,
|
||||
storedQueryError,
|
||||
showOpenSchemaActionText,
|
||||
isStoredQueryLoading,
|
||||
createQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
onOpenSchemaSidebar,
|
||||
|
|
@ -59,6 +59,10 @@ const QueryEditor = ({
|
|||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [isQuerySaving, setIsQuerySaving] = useState(false);
|
||||
|
|
@ -77,29 +81,35 @@ const QueryEditor = ({
|
|||
[key: string]: string;
|
||||
}>({});
|
||||
|
||||
const onSaveQueryFormSubmit = debounce(async (formData: IQueryFormData) => {
|
||||
const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
|
||||
setIsQuerySaving(true);
|
||||
try {
|
||||
const { query }: { query: IQuery } = await createQuery(formData);
|
||||
router.push(PATHS.EDIT_QUERY(query));
|
||||
const { query } = await queryAPI.create(formData);
|
||||
router.push(PATHS.EDIT_QUERY(query.id));
|
||||
renderFlash("success", "Query created!");
|
||||
setBackendValidators({});
|
||||
} catch (createError: any) {
|
||||
console.error(createError);
|
||||
if (createError.data.errors[0].reason.includes("already exists")) {
|
||||
setBackendValidators({ name: "A query with this name 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: IQueryFormData) => {
|
||||
const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
|
||||
if (!queryIdForEdit) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -111,6 +121,10 @@ const QueryEditor = ({
|
|||
lastEditedQueryDescription,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryObserverCanRun,
|
||||
lastEditedQueryFrequency,
|
||||
lastEditedQueryPlatforms,
|
||||
lastEditedQueryLoggingType,
|
||||
lastEditedQueryMinOsqueryVersion,
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -149,12 +163,14 @@ const QueryEditor = ({
|
|||
</div>
|
||||
<QueryForm
|
||||
router={router}
|
||||
onCreateQuery={onSaveQueryFormSubmit}
|
||||
saveQuery={saveQuery}
|
||||
goToSelectTargets={goToSelectTargets}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onUpdate={onUpdateQuery}
|
||||
storedQuery={storedQuery}
|
||||
queryIdForEdit={queryIdForEdit}
|
||||
apiTeamIdForQuery={apiTeamIdForQuery}
|
||||
teamNameForQuery={teamNameForQuery}
|
||||
isStoredQueryLoading={isStoredQueryLoading}
|
||||
showOpenSchemaActionText={showOpenSchemaActionText}
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
|
|
|
|||
|
|
@ -1,546 +0,0 @@
|
|||
/* Conditionally renders global schedule and team schedules */
|
||||
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IQuery, IFleetQueriesResponse } from "interfaces/query";
|
||||
import {
|
||||
IScheduledQuery,
|
||||
IEditScheduledQuery,
|
||||
ILoadAllGlobalScheduledQueriesResponse,
|
||||
IStoredScheduledQueriesResponse,
|
||||
} from "interfaces/scheduled_query";
|
||||
import paths from "router/paths";
|
||||
import fleetQueriesAPI from "services/entities/queries";
|
||||
import globalScheduledQueriesAPI from "services/entities/global_scheduled_queries";
|
||||
import teamScheduledQueriesAPI from "services/entities/team_scheduled_queries";
|
||||
import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import Spinner from "components/Spinner";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import TableDataError from "components/DataError";
|
||||
import MainContent from "components/MainContent";
|
||||
import ShowQueryModal from "components/modals/ShowQueryModal";
|
||||
|
||||
import ScheduleTable from "./components/ScheduleTable";
|
||||
import ScheduleEditorModal from "./components/ScheduleEditorModal";
|
||||
import RemoveScheduledQueryModal from "./components/RemoveScheduledQueryModal";
|
||||
|
||||
const baseClass = "manage-schedule-page";
|
||||
|
||||
const renderTable = (
|
||||
router: InjectedRouter,
|
||||
onRemoveScheduledQueryClick: (selectIds: number[]) => void,
|
||||
onEditScheduledQueryClick: (selectedQuery: IEditScheduledQuery) => void,
|
||||
onShowQueryClick: (selectedQuery: IEditScheduledQuery) => void,
|
||||
allScheduledQueriesList: IScheduledQuery[],
|
||||
allScheduledQueriesError: Error | null,
|
||||
toggleScheduleEditorModal: () => void,
|
||||
isOnGlobalTeam: boolean,
|
||||
selectedTeamData: ITeam | undefined,
|
||||
isLoadingGlobalScheduledQueries: boolean,
|
||||
isLoadingTeamScheduledQueries: boolean,
|
||||
errorQueries: Error | null
|
||||
): JSX.Element => {
|
||||
return allScheduledQueriesError || errorQueries ? (
|
||||
<TableDataError />
|
||||
) : (
|
||||
<ScheduleTable
|
||||
router={router}
|
||||
onRemoveScheduledQueryClick={onRemoveScheduledQueryClick}
|
||||
onEditScheduledQueryClick={onEditScheduledQueryClick}
|
||||
onShowQueryClick={onShowQueryClick}
|
||||
allScheduledQueriesList={allScheduledQueriesList}
|
||||
toggleScheduleEditorModal={toggleScheduleEditorModal}
|
||||
isOnGlobalTeam={isOnGlobalTeam}
|
||||
selectedTeamData={selectedTeamData}
|
||||
loadingInheritedQueriesTableData={isLoadingGlobalScheduledQueries}
|
||||
loadingTeamQueriesTableData={isLoadingTeamScheduledQueries}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAllTeamsTable = (
|
||||
router: InjectedRouter,
|
||||
allTeamsScheduledQueriesList: IScheduledQuery[],
|
||||
allTeamsScheduledQueriesError: Error | null,
|
||||
isOnGlobalTeam: boolean,
|
||||
selectedTeamData: ITeam | undefined,
|
||||
isLoadingGlobalScheduledQueries: boolean,
|
||||
isLoadingTeamScheduledQueries: boolean
|
||||
): JSX.Element => {
|
||||
return allTeamsScheduledQueriesError ? (
|
||||
<TableDataError />
|
||||
) : (
|
||||
<div className={`${baseClass}__all-teams-table`}>
|
||||
<ScheduleTable
|
||||
router={router}
|
||||
inheritedQueries
|
||||
allScheduledQueriesList={allTeamsScheduledQueriesList}
|
||||
isOnGlobalTeam={isOnGlobalTeam}
|
||||
selectedTeamData={selectedTeamData}
|
||||
loadingInheritedQueriesTableData={isLoadingGlobalScheduledQueries}
|
||||
loadingTeamQueriesTableData={isLoadingTeamScheduledQueries}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IFormData {
|
||||
interval: number;
|
||||
name?: string;
|
||||
shard: number;
|
||||
query?: string;
|
||||
query_id?: number;
|
||||
logging_type: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
team_id?: number;
|
||||
}
|
||||
|
||||
interface ITeamSchedulesPageProps {
|
||||
params: {
|
||||
team_id: string;
|
||||
};
|
||||
router: InjectedRouter; // v3
|
||||
route: any;
|
||||
location: any;
|
||||
}
|
||||
|
||||
const ManageSchedulePage = ({
|
||||
router,
|
||||
location,
|
||||
}: ITeamSchedulesPageProps): JSX.Element => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { MANAGE_PACKS } = paths;
|
||||
const handleAdvanced = () => router.push(MANAGE_PACKS);
|
||||
|
||||
const {
|
||||
isOnGlobalTeam,
|
||||
isPremiumTier,
|
||||
isFreeTier,
|
||||
isSandboxMode,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const {
|
||||
currentTeamId,
|
||||
isAnyTeamSelected,
|
||||
isRouteOk,
|
||||
teamIdForApi,
|
||||
userTeams,
|
||||
handleTeamChange,
|
||||
} = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: false,
|
||||
permittedAccessByTeamRole: {
|
||||
admin: true,
|
||||
maintainer: true,
|
||||
observer: false,
|
||||
observer_plus: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = useQuery<
|
||||
ILoadTeamsResponse,
|
||||
Error,
|
||||
ITeam[]
|
||||
>(["teams"], () => teamsAPI.loadAll(), {
|
||||
enabled: isRouteOk && !!isPremiumTier,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => data.teams,
|
||||
});
|
||||
|
||||
const {
|
||||
data: fleetQueries,
|
||||
isLoading: isLoadingFleetQueries,
|
||||
error: errorQueries,
|
||||
} = useQuery<IFleetQueriesResponse, Error, IQuery[]>(
|
||||
["fleetQueries"],
|
||||
() => fleetQueriesAPI.loadAll(),
|
||||
{
|
||||
enabled: isRouteOk,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
select: (data) => data.queries,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: globalScheduledQueries,
|
||||
error: globalScheduledQueriesError,
|
||||
isLoading: isLoadingGlobalScheduledQueries,
|
||||
refetch: refetchGlobalScheduledQueries,
|
||||
} = useQuery<
|
||||
ILoadAllGlobalScheduledQueriesResponse,
|
||||
Error,
|
||||
IScheduledQuery[]
|
||||
>(["globalScheduledQueries"], () => globalScheduledQueriesAPI.loadAll(), {
|
||||
enabled: isRouteOk,
|
||||
select: (data) => data.global_schedule,
|
||||
});
|
||||
|
||||
const {
|
||||
data: teamScheduledQueries,
|
||||
error: teamScheduledQueriesError,
|
||||
isLoading: isLoadingTeamScheduledQueries,
|
||||
refetch: refetchTeamScheduledQueries,
|
||||
} = useQuery<IStoredScheduledQueriesResponse, Error, IScheduledQuery[]>(
|
||||
["teamScheduledQueries", teamIdForApi],
|
||||
() => teamScheduledQueriesAPI.loadAll(teamIdForApi),
|
||||
{
|
||||
enabled: isRouteOk && isPremiumTier && !!teamIdForApi,
|
||||
select: (data) => data.scheduled,
|
||||
}
|
||||
);
|
||||
|
||||
const refetchScheduledQueries = useCallback(() => {
|
||||
refetchGlobalScheduledQueries();
|
||||
if (isAnyTeamSelected) {
|
||||
refetchTeamScheduledQueries();
|
||||
}
|
||||
}, [
|
||||
isAnyTeamSelected,
|
||||
refetchGlobalScheduledQueries,
|
||||
refetchTeamScheduledQueries,
|
||||
]);
|
||||
|
||||
const allScheduledQueriesList =
|
||||
(isAnyTeamSelected ? teamScheduledQueries : globalScheduledQueries) || [];
|
||||
const allScheduledQueriesError = isAnyTeamSelected
|
||||
? teamScheduledQueriesError
|
||||
: globalScheduledQueriesError;
|
||||
|
||||
const inheritedScheduledQueriesList = globalScheduledQueries;
|
||||
const inheritedScheduledQueriesError = globalScheduledQueriesError;
|
||||
|
||||
const inheritedQueryOrQueries =
|
||||
inheritedScheduledQueriesList?.length === 1 ? "query" : "queries";
|
||||
|
||||
const selectedTeamData = isAnyTeamSelected
|
||||
? teams?.find((team: ITeam) => teamIdForApi === team.id)
|
||||
: undefined;
|
||||
|
||||
const [isUpdatingScheduledQuery, setIsUpdatingScheduledQuery] = useState(
|
||||
false
|
||||
);
|
||||
const [showInheritedQueries, setShowInheritedQueries] = useState(false);
|
||||
const [showScheduleEditorModal, setShowScheduleEditorModal] = useState(false);
|
||||
const [showShowQueryModal, setShowShowQueryModal] = useState(false);
|
||||
const [showPreviewDataModal, setShowPreviewDataModal] = useState(false);
|
||||
const [
|
||||
showRemoveScheduledQueryModal,
|
||||
setShowRemoveScheduledQueryModal,
|
||||
] = useState(false);
|
||||
const [selectedQueryIds, setSelectedQueryIds] = useState<number[] | never[]>(
|
||||
[]
|
||||
);
|
||||
const [
|
||||
selectedScheduledQuery,
|
||||
setSelectedScheduledQuery,
|
||||
] = useState<IEditScheduledQuery>();
|
||||
|
||||
const toggleInheritedQueries = () => {
|
||||
setShowInheritedQueries(!showInheritedQueries);
|
||||
};
|
||||
|
||||
const togglePreviewDataModal = useCallback(() => {
|
||||
setShowPreviewDataModal(!showPreviewDataModal);
|
||||
}, [setShowPreviewDataModal, showPreviewDataModal]);
|
||||
|
||||
const toggleScheduleEditorModal = useCallback(() => {
|
||||
setSelectedScheduledQuery(undefined); // create modal renders
|
||||
setShowScheduleEditorModal(!showScheduleEditorModal);
|
||||
}, [showScheduleEditorModal, setShowScheduleEditorModal]);
|
||||
|
||||
const toggleShowQueryModal = useCallback(() => {
|
||||
setSelectedScheduledQuery(undefined);
|
||||
setShowShowQueryModal(!showShowQueryModal);
|
||||
}, [showShowQueryModal, setShowShowQueryModal]);
|
||||
|
||||
const toggleRemoveScheduledQueryModal = useCallback(() => {
|
||||
setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal);
|
||||
}, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]);
|
||||
|
||||
const onRemoveScheduledQueryClick = (
|
||||
selectedTableQueryIds: number[]
|
||||
): void => {
|
||||
toggleRemoveScheduledQueryModal();
|
||||
setSelectedQueryIds(selectedTableQueryIds);
|
||||
};
|
||||
|
||||
const onShowQueryClick = (selectedQuery: IEditScheduledQuery): void => {
|
||||
toggleShowQueryModal();
|
||||
setSelectedScheduledQuery(selectedQuery);
|
||||
};
|
||||
|
||||
const onEditScheduledQueryClick = (
|
||||
selectedQuery: IEditScheduledQuery
|
||||
): void => {
|
||||
toggleScheduleEditorModal();
|
||||
setSelectedScheduledQuery(selectedQuery); // edit modal renders
|
||||
};
|
||||
|
||||
const onRemoveScheduledQuerySubmit = useCallback(() => {
|
||||
setIsUpdatingScheduledQuery(true);
|
||||
const promises = selectedQueryIds.map((id: number) => {
|
||||
return isAnyTeamSelected
|
||||
? teamScheduledQueriesAPI.destroy(teamIdForApi, id)
|
||||
: globalScheduledQueriesAPI.destroy({ id });
|
||||
});
|
||||
const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries";
|
||||
return Promise.all(promises)
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully removed scheduled ${queryOrQueries}.`
|
||||
);
|
||||
toggleRemoveScheduledQueryModal();
|
||||
refetchScheduledQueries();
|
||||
})
|
||||
.catch(() => {
|
||||
renderFlash(
|
||||
"error",
|
||||
`Unable to remove scheduled ${queryOrQueries}. Please try again.`
|
||||
);
|
||||
toggleRemoveScheduledQueryModal();
|
||||
})
|
||||
.finally(() => {
|
||||
refetchGlobalScheduledQueries();
|
||||
setIsUpdatingScheduledQuery(false);
|
||||
});
|
||||
}, [
|
||||
selectedQueryIds,
|
||||
isAnyTeamSelected,
|
||||
teamIdForApi,
|
||||
renderFlash,
|
||||
toggleRemoveScheduledQueryModal,
|
||||
refetchScheduledQueries,
|
||||
refetchGlobalScheduledQueries,
|
||||
]);
|
||||
|
||||
const onAddScheduledQuerySubmit = useCallback(
|
||||
(formData: IFormData, editQuery: IEditScheduledQuery | undefined) => {
|
||||
setIsUpdatingScheduledQuery(true);
|
||||
if (editQuery) {
|
||||
const updatedAttributes = deepDifference(formData, editQuery);
|
||||
|
||||
const editResponse =
|
||||
editQuery.type === "team_scheduled_query"
|
||||
? teamScheduledQueriesAPI.update(editQuery, updatedAttributes)
|
||||
: globalScheduledQueriesAPI.update(editQuery, updatedAttributes);
|
||||
|
||||
editResponse
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully updated ${formData.name} in the schedule.`
|
||||
);
|
||||
refetchScheduledQueries();
|
||||
toggleScheduleEditorModal();
|
||||
})
|
||||
.catch(() => {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Could not update scheduled query. Please try again."
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdatingScheduledQuery(false);
|
||||
refetchGlobalScheduledQueries();
|
||||
});
|
||||
} else {
|
||||
const createResponse = isAnyTeamSelected
|
||||
? teamScheduledQueriesAPI.create({ ...formData })
|
||||
: globalScheduledQueriesAPI.create({ ...formData });
|
||||
|
||||
createResponse
|
||||
.then(() => {
|
||||
renderFlash(
|
||||
"success",
|
||||
`Successfully added ${formData.name} to the schedule.`
|
||||
);
|
||||
refetchScheduledQueries();
|
||||
toggleScheduleEditorModal();
|
||||
})
|
||||
.catch(() => {
|
||||
renderFlash("error", "Could not schedule query. Please try again.");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdatingScheduledQuery(false);
|
||||
refetchGlobalScheduledQueries();
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
isAnyTeamSelected,
|
||||
refetchGlobalScheduledQueries,
|
||||
refetchScheduledQueries,
|
||||
renderFlash,
|
||||
toggleScheduleEditorModal,
|
||||
]
|
||||
);
|
||||
|
||||
if (!isRouteOk || (isPremiumTier && !userTeams?.length)) {
|
||||
return (
|
||||
<div className={`${baseClass}__loading-spinner`}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__text`}>
|
||||
<div className={`${baseClass}__title`}>
|
||||
{isFreeTier && <h1>Schedule</h1>}
|
||||
{isPremiumTier &&
|
||||
userTeams &&
|
||||
(userTeams.length > 1 || isOnGlobalTeam) && (
|
||||
<TeamsDropdown
|
||||
selectedTeamId={currentTeamId}
|
||||
currentUserTeams={userTeams || []}
|
||||
onChange={handleTeamChange}
|
||||
isSandboxMode={isSandboxMode}
|
||||
/>
|
||||
)}
|
||||
{isPremiumTier &&
|
||||
!isOnGlobalTeam &&
|
||||
userTeams &&
|
||||
userTeams.length === 1 && <h1>{userTeams[0].name}</h1>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{allScheduledQueriesList?.length !== 0 && !allScheduledQueriesError && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{/* NOTE: Product decision to remove packs from UI
|
||||
{isOnGlobalTeam && (
|
||||
<Button
|
||||
variant="inverse"
|
||||
onClick={handleAdvanced}
|
||||
className={`${baseClass}__advanced-button`}
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
)} */}
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__schedule-button`}
|
||||
onClick={toggleScheduleEditorModal}
|
||||
>
|
||||
Schedule a query
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
{!isLoadingTeams && (
|
||||
<div>
|
||||
{isAnyTeamSelected ? (
|
||||
<p>
|
||||
Schedule queries for{" "}
|
||||
<strong>all hosts assigned to this team</strong>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Schedule queries to run at regular intervals across{" "}
|
||||
<strong>all of your hosts</strong>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{isLoadingTeams ||
|
||||
isLoadingFleetQueries ||
|
||||
isLoadingGlobalScheduledQueries ||
|
||||
isLoadingTeamScheduledQueries ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
renderTable(
|
||||
router,
|
||||
onRemoveScheduledQueryClick,
|
||||
onEditScheduledQueryClick,
|
||||
onShowQueryClick,
|
||||
allScheduledQueriesList,
|
||||
allScheduledQueriesError,
|
||||
toggleScheduleEditorModal,
|
||||
isOnGlobalTeam || false,
|
||||
selectedTeamData,
|
||||
isLoadingGlobalScheduledQueries,
|
||||
isLoadingTeamScheduledQueries,
|
||||
errorQueries
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{/* must use ternary for NaN */}
|
||||
{isAnyTeamSelected &&
|
||||
inheritedScheduledQueriesList &&
|
||||
inheritedScheduledQueriesList.length > 0 ? (
|
||||
<RevealButton
|
||||
isShowing={showInheritedQueries}
|
||||
className={baseClass}
|
||||
hideText={`Hide ${inheritedScheduledQueriesList.length} inherited ${inheritedQueryOrQueries}`}
|
||||
showText={`Show ${inheritedScheduledQueriesList.length} inherited ${inheritedQueryOrQueries}`}
|
||||
caretPosition={"before"}
|
||||
tooltipHtml={
|
||||
'Queries from the "All teams"<br/>schedule run on this team’s hosts.'
|
||||
}
|
||||
onClick={toggleInheritedQueries}
|
||||
/>
|
||||
) : null}
|
||||
{showInheritedQueries &&
|
||||
inheritedScheduledQueriesList &&
|
||||
renderAllTeamsTable(
|
||||
router,
|
||||
inheritedScheduledQueriesList,
|
||||
inheritedScheduledQueriesError,
|
||||
isOnGlobalTeam || false,
|
||||
selectedTeamData,
|
||||
isLoadingGlobalScheduledQueries,
|
||||
isLoadingTeamScheduledQueries
|
||||
)}
|
||||
{showScheduleEditorModal && fleetQueries && (
|
||||
<ScheduleEditorModal
|
||||
onClose={toggleScheduleEditorModal}
|
||||
onScheduleSubmit={onAddScheduledQuerySubmit}
|
||||
allQueries={fleetQueries}
|
||||
editQuery={selectedScheduledQuery}
|
||||
teamId={teamIdForApi}
|
||||
togglePreviewDataModal={togglePreviewDataModal}
|
||||
showPreviewDataModal={showPreviewDataModal}
|
||||
isUpdatingScheduledQuery={isUpdatingScheduledQuery}
|
||||
/>
|
||||
)}
|
||||
{showRemoveScheduledQueryModal && (
|
||||
<RemoveScheduledQueryModal
|
||||
onCancel={toggleRemoveScheduledQueryModal}
|
||||
onSubmit={onRemoveScheduledQuerySubmit}
|
||||
isUpdatingScheduledQuery={isUpdatingScheduledQuery}
|
||||
/>
|
||||
)}
|
||||
{showShowQueryModal && (
|
||||
<ShowQueryModal
|
||||
query={selectedScheduledQuery?.query}
|
||||
onCancel={toggleShowQueryModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</MainContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageSchedulePage;
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
.manage-schedule-page {
|
||||
&__header-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-right: $pad-large;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $large;
|
||||
|
||||
.fleeticon {
|
||||
color: $core-fleet-blue;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.fleeticon-success-check {
|
||||
color: $ui-success;
|
||||
}
|
||||
|
||||
.fleeticon-offline {
|
||||
color: $ui-error;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
color: $core-fleet-black;
|
||||
font-weight: $regular;
|
||||
font-size: $small;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $ui-fleet-black-75;
|
||||
margin: 0;
|
||||
font-size: $x-small;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-button-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__advanced-button {
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
|
||||
.Select.is-open {
|
||||
.Select-value-label {
|
||||
color: $core-vibrant-blue !important;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-table {
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
thead {
|
||||
.query_name__header {
|
||||
width: $col-lg;
|
||||
}
|
||||
.interval__header {
|
||||
width: auto;
|
||||
}
|
||||
.actions__header {
|
||||
width: auto;
|
||||
}
|
||||
@media (min-width: $break-lg) {
|
||||
.interval__header {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
.query_name__cell {
|
||||
width: $col-lg;
|
||||
max-width: 175px; // Truncates at smaller widths
|
||||
}
|
||||
.interval__cell {
|
||||
width: auto;
|
||||
}
|
||||
.actions__cell {
|
||||
width: auto;
|
||||
}
|
||||
@media (min-width: $break-lg) {
|
||||
.interval_cell {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-table__container {
|
||||
max-width: 465px; // Fixes wider font causing orphaned word on all teams empty state
|
||||
}
|
||||
}
|
||||
|
||||
.no-team-schedule {
|
||||
border: 1px solid #e2e4ea;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
.preview-data-modal {
|
||||
&__sandbox-info {
|
||||
margin-top: $pad-medium;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
const baseClass = "remove-scheduled-query-modal";
|
||||
|
||||
interface IRemoveScheduledQueryModalProps {
|
||||
isUpdatingScheduledQuery: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const RemoveScheduledQueryModal = ({
|
||||
isUpdatingScheduledQuery,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: IRemoveScheduledQueryModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal
|
||||
title={"Remove scheduled query"}
|
||||
onExit={onCancel}
|
||||
onEnter={onSubmit}
|
||||
className={baseClass}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to remove the selected queries from the schedule?
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
variant="alert"
|
||||
onClick={onSubmit}
|
||||
className="remove-loading"
|
||||
isLoading={isUpdatingScheduledQuery}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveScheduledQueryModal;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./RemoveScheduledQueryModal";
|
||||
|
|
@ -1,364 +0,0 @@
|
|||
/* This component is used for creating and editing both global and team scheduled queries */
|
||||
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import { pull } from "lodash";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { IQuery } from "interfaces/query";
|
||||
import { IEditScheduledQuery } from "interfaces/scheduled_query";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
|
||||
LOGGING_TYPE_OPTIONS,
|
||||
MIN_OSQUERY_VERSION_OPTIONS,
|
||||
} from "utilities/constants";
|
||||
|
||||
import PreviewDataModal from "../PreviewDataModal";
|
||||
|
||||
const baseClass = "schedule-editor-modal";
|
||||
|
||||
interface IFormData {
|
||||
interval: number;
|
||||
name?: string;
|
||||
shard: number;
|
||||
query?: string;
|
||||
query_id?: number;
|
||||
logging_type: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
team_id?: number;
|
||||
}
|
||||
|
||||
interface IScheduleEditorModalProps {
|
||||
allQueries: IQuery[];
|
||||
onClose: () => void;
|
||||
onScheduleSubmit: (
|
||||
formData: IFormData,
|
||||
editQuery: IEditScheduledQuery | undefined
|
||||
) => void;
|
||||
editQuery?: IEditScheduledQuery;
|
||||
teamId?: number;
|
||||
togglePreviewDataModal: () => void;
|
||||
showPreviewDataModal: boolean;
|
||||
isUpdatingScheduledQuery: boolean;
|
||||
}
|
||||
interface INoQueryOption {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const generateLoggingType = (query: IEditScheduledQuery) => {
|
||||
if (query.snapshot) {
|
||||
return "snapshot";
|
||||
}
|
||||
if (query.removed) {
|
||||
return "differential";
|
||||
}
|
||||
return "differential_ignore_removals";
|
||||
};
|
||||
|
||||
const generateLoggingDestination = (loggingConfig: string): string => {
|
||||
switch (loggingConfig) {
|
||||
case "filesystem":
|
||||
return "the filesystem";
|
||||
case "firehose":
|
||||
return "AWS Kinesis Firehose";
|
||||
case "kinesis":
|
||||
return "AWS Kinesis";
|
||||
case "lambda":
|
||||
return "AWS Lambda";
|
||||
case "pubsub":
|
||||
return "GCP PubSub";
|
||||
case "stdout":
|
||||
return "the standard output stream";
|
||||
default:
|
||||
return loggingConfig;
|
||||
}
|
||||
};
|
||||
|
||||
const ScheduleEditorModal = ({
|
||||
onClose,
|
||||
onScheduleSubmit,
|
||||
allQueries,
|
||||
editQuery,
|
||||
teamId,
|
||||
togglePreviewDataModal,
|
||||
showPreviewDataModal,
|
||||
isUpdatingScheduledQuery,
|
||||
}: IScheduleEditorModalProps): JSX.Element => {
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const loggingConfig = config?.logging.result.plugin || "unknown";
|
||||
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [selectedQuery, setSelectedQuery] = useState<
|
||||
IEditScheduledQuery | INoQueryOption
|
||||
>();
|
||||
const [selectedFrequency, setSelectedFrequency] = useState(
|
||||
editQuery ? editQuery.interval : 86400
|
||||
);
|
||||
const [selectedPlatformOptions, setSelectedPlatformOptions] = useState(
|
||||
editQuery?.platform || ""
|
||||
);
|
||||
const [selectedLoggingType, setSelectedLoggingType] = useState(
|
||||
editQuery ? generateLoggingType(editQuery) : "snapshot"
|
||||
);
|
||||
const [
|
||||
selectedMinOsqueryVersionOptions,
|
||||
setSelectedMinOsqueryVersionOptions,
|
||||
] = useState(editQuery?.version || "");
|
||||
const [selectedShard, setSelectedShard] = useState(
|
||||
editQuery?.shard ? editQuery?.shard.toString() : ""
|
||||
);
|
||||
|
||||
const createQueryDropdownOptions = () => {
|
||||
const queryOptions = allQueries.map((q) => {
|
||||
return {
|
||||
value: String(q.id),
|
||||
label: q.name,
|
||||
};
|
||||
});
|
||||
return queryOptions;
|
||||
};
|
||||
|
||||
const toggleAdvancedOptions = () => {
|
||||
setShowAdvancedOptions(!showAdvancedOptions);
|
||||
};
|
||||
|
||||
const onChangeSelectQuery = useCallback(
|
||||
(queryId: string) => {
|
||||
const queryWithId: IQuery | undefined = allQueries.find(
|
||||
(query: IQuery) => query.id === parseInt(queryId, 10)
|
||||
);
|
||||
setSelectedQuery(queryWithId);
|
||||
},
|
||||
[allQueries, setSelectedQuery]
|
||||
);
|
||||
|
||||
const onChangeSelectFrequency = useCallback(
|
||||
(value: number) => {
|
||||
setSelectedFrequency(value);
|
||||
},
|
||||
[setSelectedFrequency]
|
||||
);
|
||||
|
||||
const onChangeSelectPlatformOptions = useCallback(
|
||||
(values: string) => {
|
||||
const valArray = values.split(",");
|
||||
|
||||
// Remove All if another OS is chosen
|
||||
// else if Remove OS if All is chosen
|
||||
if (valArray.indexOf("") === 0 && valArray.length > 1) {
|
||||
setSelectedPlatformOptions(pull(valArray, "").join(","));
|
||||
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
|
||||
setSelectedPlatformOptions("");
|
||||
} else {
|
||||
setSelectedPlatformOptions(values);
|
||||
}
|
||||
},
|
||||
[setSelectedPlatformOptions]
|
||||
);
|
||||
|
||||
const onChangeSelectLoggingType = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedLoggingType(value);
|
||||
},
|
||||
[setSelectedLoggingType]
|
||||
);
|
||||
|
||||
const onChangeMinOsqueryVersionOptions = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedMinOsqueryVersionOptions(value);
|
||||
},
|
||||
[setSelectedMinOsqueryVersionOptions]
|
||||
);
|
||||
|
||||
const onChangeShard = useCallback(
|
||||
(value: string) => {
|
||||
setSelectedShard(value);
|
||||
},
|
||||
[setSelectedShard]
|
||||
);
|
||||
|
||||
const onFormSubmit = (): void => {
|
||||
const query_id = () => {
|
||||
if (editQuery) {
|
||||
return editQuery.query_id;
|
||||
}
|
||||
return selectedQuery?.id;
|
||||
};
|
||||
|
||||
const name = () => {
|
||||
if (editQuery) {
|
||||
return editQuery.name;
|
||||
}
|
||||
return selectedQuery?.name;
|
||||
};
|
||||
|
||||
onScheduleSubmit(
|
||||
{
|
||||
shard: parseInt(selectedShard, 10),
|
||||
interval: selectedFrequency,
|
||||
query_id: query_id(),
|
||||
name: name(),
|
||||
logging_type: selectedLoggingType,
|
||||
platform: selectedPlatformOptions,
|
||||
version: selectedMinOsqueryVersionOptions,
|
||||
team_id: teamId,
|
||||
},
|
||||
editQuery
|
||||
);
|
||||
};
|
||||
|
||||
if (showPreviewDataModal) {
|
||||
return <PreviewDataModal onCancel={togglePreviewDataModal} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={editQuery?.query_name || "Schedule editor"}
|
||||
onExit={onClose}
|
||||
onEnter={onFormSubmit}
|
||||
className={baseClass}
|
||||
width="large"
|
||||
>
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p className={`${baseClass}__platform-compatibility`}>
|
||||
Scheduled queries can currently be run on macOS, Windows, and Linux
|
||||
hosts. Interested in collecting data from your Chromebooks?{" "}
|
||||
<CustomLink
|
||||
url="https://www.fleetdm.com/contact"
|
||||
text="Let us know"
|
||||
newTab
|
||||
/>
|
||||
</p>
|
||||
{!editQuery && (
|
||||
<Dropdown
|
||||
searchable
|
||||
options={createQueryDropdownOptions()}
|
||||
onChange={onChangeSelectQuery}
|
||||
placeholder={"Select query"}
|
||||
value={selectedQuery?.id}
|
||||
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={onChangeSelectFrequency}
|
||||
placeholder={"Every day"}
|
||||
value={selectedFrequency}
|
||||
label={"Choose a frequency and then run this query on a schedule"}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
/>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your configured log destination is <b>{loggingConfig}</b>.
|
||||
</p>
|
||||
<p>
|
||||
{loggingConfig === "unknown"
|
||||
? ""
|
||||
: `This means that when this query is run on your hosts, the data will
|
||||
be sent to ${generateLoggingDestination(loggingConfig)}.`}
|
||||
</p>
|
||||
<p>
|
||||
Check out the Fleet documentation on
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/deploying/configuration#osquery-result-log-plugin"
|
||||
text="how to configure a different log destination"
|
||||
newTab
|
||||
multiline
|
||||
/>
|
||||
.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className={baseClass}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div>
|
||||
<Dropdown
|
||||
options={LOGGING_TYPE_OPTIONS}
|
||||
onChange={onChangeSelectLoggingType}
|
||||
placeholder="Select"
|
||||
value={selectedLoggingType}
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={SCHEDULE_PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platform"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={selectedPlatformOptions}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={onChangeMinOsqueryVersionOptions}
|
||||
placeholder="Select"
|
||||
value={selectedMinOsqueryVersionOptions}
|
||||
label="Minimum osquery version"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<InputField
|
||||
onChange={onChangeShard}
|
||||
inputWrapperClass={`${baseClass}__form-field ${baseClass}__form-field--shard`}
|
||||
value={selectedShard}
|
||||
placeholder="- - -"
|
||||
label="Shard"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<div className={`${baseClass}__preview-btn-wrap`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="inverse"
|
||||
onClick={togglePreviewDataModal}
|
||||
>
|
||||
Preview data
|
||||
</Button>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
variant="brand"
|
||||
onClick={onFormSubmit}
|
||||
disabled={!selectedQuery && !editQuery}
|
||||
className="schedule-loading"
|
||||
isLoading={isUpdatingScheduledQuery}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleEditorModal;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
.schedule-editor-modal {
|
||||
&__platform-compatibility {
|
||||
margin-bottom: $pad-large;
|
||||
}
|
||||
|
||||
&__sandbox-info {
|
||||
margin-top: $pad-medium;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__info-header {
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
font-size: $small;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./ScheduleEditorModal";
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
/**
|
||||
* Component when there is an error retrieving schedule set up in fleet
|
||||
*/
|
||||
import React from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import paths from "router/paths";
|
||||
|
||||
import {
|
||||
IScheduledQuery,
|
||||
IEditScheduledQuery,
|
||||
} from "interfaces/scheduled_query";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IEmptyTableProps } from "interfaces/empty_table";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TableContainer from "components/TableContainer";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import {
|
||||
generateInheritedQueriesTableHeaders,
|
||||
generateTableHeaders,
|
||||
generateDataSet,
|
||||
} from "./ScheduleTableConfig";
|
||||
|
||||
const baseClass = "schedule-table";
|
||||
|
||||
const TAGGED_TEMPLATES = {
|
||||
hostsByTeamRoute: (teamId: number | undefined | null) => {
|
||||
return `${teamId ? `/?team_id=${teamId}` : ""}`;
|
||||
},
|
||||
};
|
||||
interface IScheduleTableProps {
|
||||
router: InjectedRouter; // v3
|
||||
onRemoveScheduledQueryClick?: (selectedIds: number[]) => void;
|
||||
onEditScheduledQueryClick?: (selectedQuery: IEditScheduledQuery) => void;
|
||||
onShowQueryClick?: (selectedQuery: IEditScheduledQuery) => void;
|
||||
allScheduledQueriesList: IScheduledQuery[];
|
||||
toggleScheduleEditorModal?: () => void;
|
||||
inheritedQueries?: boolean;
|
||||
isOnGlobalTeam: boolean;
|
||||
selectedTeamData: ITeam | undefined;
|
||||
loadingInheritedQueriesTableData: boolean;
|
||||
loadingTeamQueriesTableData: boolean;
|
||||
}
|
||||
|
||||
const ScheduleTable = ({
|
||||
router,
|
||||
onRemoveScheduledQueryClick,
|
||||
onEditScheduledQueryClick,
|
||||
onShowQueryClick,
|
||||
allScheduledQueriesList,
|
||||
toggleScheduleEditorModal,
|
||||
inheritedQueries,
|
||||
isOnGlobalTeam,
|
||||
selectedTeamData,
|
||||
loadingInheritedQueriesTableData,
|
||||
loadingTeamQueriesTableData,
|
||||
}: IScheduleTableProps): JSX.Element => {
|
||||
const { MANAGE_PACKS, MANAGE_HOSTS } = paths;
|
||||
|
||||
const handleAdvanced = () => router.push(MANAGE_PACKS);
|
||||
|
||||
const emptyState = () => {
|
||||
const emptySchedule: IEmptyTableProps = {
|
||||
iconName: "empty-schedule",
|
||||
header: (
|
||||
<>
|
||||
Schedule queries to run at regular intervals on{" "}
|
||||
<a href={MANAGE_HOSTS}>all your hosts</a>
|
||||
</>
|
||||
),
|
||||
additionalInfo: (
|
||||
<>
|
||||
Want to learn more?
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/fleet-ui#schedule-a-query"
|
||||
text="Read about scheduling a query"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
),
|
||||
primaryButton: (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__schedule-button`}
|
||||
onClick={toggleScheduleEditorModal}
|
||||
>
|
||||
Schedule a query
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
|
||||
if (selectedTeamData) {
|
||||
emptySchedule.header = (
|
||||
<>
|
||||
Schedule queries for all hosts assigned to{" "}
|
||||
<a
|
||||
href={
|
||||
MANAGE_HOSTS +
|
||||
TAGGED_TEMPLATES.hostsByTeamRoute(selectedTeamData.id)
|
||||
}
|
||||
>
|
||||
{selectedTeamData.name}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* NOTE: Product decision to remove packs from UI
|
||||
if (isOnGlobalTeam) {
|
||||
emptySchedule.info = (
|
||||
<>Or go to your osquery packs via the ‘Advanced’ button. </>
|
||||
);
|
||||
emptySchedule.secondaryButton = (
|
||||
<Button
|
||||
variant="inverse"
|
||||
onClick={handleAdvanced}
|
||||
className={`${baseClass}__advanced-button`}
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
*/
|
||||
return emptySchedule;
|
||||
};
|
||||
|
||||
const onActionSelection = (
|
||||
action: string,
|
||||
scheduledQuery: IEditScheduledQuery
|
||||
): void => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
if (onEditScheduledQueryClick) {
|
||||
onEditScheduledQueryClick(scheduledQuery);
|
||||
}
|
||||
break;
|
||||
case "showQuery":
|
||||
if (onShowQueryClick) {
|
||||
onShowQueryClick(scheduledQuery);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (onRemoveScheduledQueryClick) {
|
||||
onRemoveScheduledQueryClick([scheduledQuery.id]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const tableHeaders = generateTableHeaders(onActionSelection);
|
||||
const loadingTableData = selectedTeamData?.id
|
||||
? loadingTeamQueriesTableData
|
||||
: loadingInheritedQueriesTableData;
|
||||
|
||||
if (inheritedQueries) {
|
||||
const inheritedQueriesTableHeaders = generateInheritedQueriesTableHeaders();
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<TableContainer
|
||||
resultsTitle={"queries"}
|
||||
columns={inheritedQueriesTableHeaders}
|
||||
data={generateDataSet(allScheduledQueriesList, selectedTeamData?.id)}
|
||||
isLoading={loadingInheritedQueriesTableData}
|
||||
defaultSortHeader={"query"}
|
||||
defaultSortDirection={"desc"}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
searchable={false}
|
||||
disablePagination
|
||||
disableCount
|
||||
emptyComponent={() =>
|
||||
EmptyTable({
|
||||
iconName: emptyState().iconName,
|
||||
header: emptyState().header,
|
||||
info: emptyState().info,
|
||||
additionalInfo: emptyState().additionalInfo,
|
||||
primaryButton: emptyState().primaryButton,
|
||||
secondaryButton: emptyState().secondaryButton,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<TableContainer
|
||||
resultsTitle={"queries"}
|
||||
columns={tableHeaders}
|
||||
data={generateDataSet(allScheduledQueriesList, selectedTeamData?.id)}
|
||||
isLoading={loadingTableData}
|
||||
defaultSortHeader={"query"}
|
||||
defaultSortDirection={"desc"}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
inputPlaceHolder="Search"
|
||||
searchable={false}
|
||||
primarySelectAction={{
|
||||
name: "remove scheduled query",
|
||||
buttonText: "Remove",
|
||||
iconSvg: "ex",
|
||||
variant: "text-icon",
|
||||
onActionButtonClick: onRemoveScheduledQueryClick,
|
||||
}}
|
||||
emptyComponent={() =>
|
||||
EmptyTable({
|
||||
iconName: emptyState().iconName,
|
||||
header: emptyState().header,
|
||||
info: emptyState().info,
|
||||
additionalInfo: emptyState().additionalInfo,
|
||||
primaryButton: emptyState().primaryButton,
|
||||
secondaryButton: emptyState().secondaryButton,
|
||||
})
|
||||
}
|
||||
isClientSidePagination
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleTable;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue