diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 7955082fc7e8..4819eabc22f9 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -2,7 +2,7 @@ name: 💡  Feature request about: Propose a new feature or enhancement in Fleet. title: '' -labels: '~feature fest,:product' +labels: ':product' assignees: '' --- diff --git a/.github/workflows/check-tuf-timestamps.yml b/.github/workflows/check-tuf-timestamps.yml index c55de6e2b960..cdd98c7d91cc 100644 --- a/.github/workflows/check-tuf-timestamps.yml +++ b/.github/workflows/check-tuf-timestamps.yml @@ -6,7 +6,7 @@ on: - '.github/workflows/check-tuf-timestamps.yml' workflow_dispatch: # Manual schedule: - - cron: '0 10 * * *' + - cron: '0 10,22 * * *' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: @@ -38,7 +38,7 @@ jobs: run: | expires=$(curl -s http://tuf.fleetctl.com/timestamp.json | jq -r '.signed.expires' | cut -c 1-10) today=$(date "+%Y-%m-%d") - warning_at=$(date -d "$today + 2 day" "+%Y-%m-%d") + warning_at=$(date -d "$today + 4 day" "+%Y-%m-%d") expires_sec=$(date -d "$expires" "+%s") warning_at_sec=$(date -d "$warning_at" "+%s") diff --git a/.github/workflows/render-deploy.yml b/.github/workflows/render-deploy.yml new file mode 100644 index 000000000000..1a927ed3bd8e --- /dev/null +++ b/.github/workflows/render-deploy.yml @@ -0,0 +1,48 @@ +name: Render deploy + +# Re-deploy Fleet servers on Render to update to the latest Fleet release. +# +# Render (https://render.com/) is hosting 2 Fleet servers that are used by our gitops repos: +# - https://github.com/fleetdm/fleet-gitops +# - https://gitlab.com/fleetdm/fleet-gitops +# +# The premium server (fleet-gitops-ci-premium) is used by GitHub CI and the free server (fleet-gitops-ci-free) is used by GitLab CI. +# Both servers share a MySQL service (fleet-gitops-ci-mysql). +# - fleet-gitops-ci-premium uses fleet database +# - fleet-gitops-ci-free uses fleet_free database +# +# Both servers share a Redis service (fleet-gitops-ci-redis). +# - fleet-gitops-ci-premium uses database 0 (the default) +# - fleet-gitops-ci-free uses database 1 + +on: + workflow_dispatch: # Manual + schedule: + - cron: '0 2 * * *' # Nightly 2AM UTC + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}} + cancel-in-progress: true + +defaults: + run: + # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference + shell: bash + +permissions: + contents: read + +jobs: + render-deploy: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: Trigger deploy + run: | + curl "${{ secrets.RENDER_GITOPS_FREE_DEPLOY_HOOK }}" + curl "${{ secrets.RENDER_GITOPS_PREMIUM_DEPLOY_HOOK }}" diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml index b9c013bd4101..084b1ea56b53 100644 --- a/.github/workflows/test-go.yaml +++ b/.github/workflows/test-go.yaml @@ -44,7 +44,7 @@ jobs: matrix: suite: ["integration", "core"] os: [ubuntu-latest] - mysql: ["mysql:8.0.36", "mysql:8.4.2"] # make sure to update supported versions docs when this changes + mysql: ["mysql:8.0.36", "mysql:8.4.3"] # make sure to update supported versions docs when this changes continue-on-error: ${{ matrix.suite == 'integration' }} # Since integration tests have a higher chance of failing, often for unrelated reasons, we don't want to fail the whole job if they fail runs-on: ${{ matrix.os }} diff --git a/articles/puppet-module.md b/articles/puppet-module.md index bf6a442bc14e..78dc4b0c7490 100644 --- a/articles/puppet-module.md +++ b/articles/puppet-module.md @@ -20,7 +20,7 @@ Install [Fleet's Puppet module](https://forge.puppet.com/modules/fleetdm/fleetdm ### Step 2: configure Puppet to talk to Fleet using Heira -1. In Fleet, create an API-only user with the global admin role. Instructions for creating an API-only user are [here](./fleetctl-CLI.md#create-an-api-only-user). +1. In Fleet, create an API-only user with the GitOps role. Instructions for creating an API-only user are [here](./fleetctl-CLI.md#create-an-api-only-user). 2. Get the API token for your new API-only user. Learn how [here](./fleetctl-CLI.md#get-the-api-token-of-an-api-only-user). diff --git a/articles/seamless-mdm-migration.md b/articles/seamless-mdm-migration.md index 9abf3c516c4f..369c6c17145d 100644 --- a/articles/seamless-mdm-migration.md +++ b/articles/seamless-mdm-migration.md @@ -1,20 +1,20 @@ -# Seamless MDM migrations to Fleet +# Seamless macOS MDM migration -![Seamless MDM migrations to Fleet](../website/assets/images/articles/seamless-mdm-migration-1600x900@2x.png) +![Seamless macOS MDM migrations to Fleet](../website/assets/images/articles/seamless-mdm-migration-1600x900@2x.png) Migrating macOS devices between Mobile Device Management (MDM) solutions is often fraught with challenges, including potential gaps in device management, user disruption, and compliance issues. Traditional MDM migrations typically require end-user interaction and leave devices unmanaged for a period, leading to problems like Wi-Fi disconnections due to certificate profile removal and incomplete migrations. These challenges can force organizations to stay with outdated MDM solutions that no longer meet their needs. But there’s a better way. Seamless MDM migrations are now possible, allowing organizations to transition their macOS devices to Fleet without any downtime or end-user involvement. By leveraging Fleet, you can ensure that your devices remain fully managed and compliant throughout the migration process. This means no more gaps in management, no user disruptions, and a smoother path to a more modern and effective MDM solution. -This guide will walk you through the entire process of migrating your MDM deployment to Fleet. You’ll start by understanding the specific requirements for a seamless migration, followed by configuring Fleet with the necessary certificates and database records. The guide will then take you through the process of installing Fleet’s agent (`fleetd`) on your devices, updating DNS records to redirect devices to the Fleet server, and finally, decommissioning your old MDM server. +This guide will walk you through the entire process of migrating your MDM deployment to Fleet. You’ll start by understanding the specific requirements for a seamless migration, followed by configuring Fleet with the necessary certificates and database records. The guide will then take you through the process of installing Fleet’s agent (`fleetd`) on your devices, updating domain (DNS) records to redirect devices to the Fleet server, and finally, decommissioning your old MDM server. Throughout the guide, you’ll find practical advice and best practices to ensure a smooth transition with minimal risk. By the end, you’ll be equipped with the knowledge and tools to execute a seamless MDM migration to Fleet, ensuring that your organization’s devices are securely managed without the typical headaches associated with a traditional MDM switch. ## Requirements -Note: Deployments that do not meet these seamless migration requirements can still migrate with the [standard MDM migration process](https://fleetdm.com/docs/using-fleet/mdm-migration-guide). +> Deployments that do not meet these seamless migration requirements can still migrate with the [standard MDM migration process](https://fleetdm.com/docs/using-fleet/mdm-migration-guide). -* Customer controls the DNS used in the MDM server enrollment (eg. devices are enrolled to `*.customerowneddomain.com`, not `*.mdmvendor.com`). +* Customer owns the domain (DNS) used in the MDM enrollment profile (e.g. devices are enrolled to `*.customerowneddomain.com`, not `*.mdmvendor.com`). * Customer has access to the Apple Push Notification Service (APNS) certificate/key and SCEP certificate/key, or access to the MDM server database to extract these values. These requirements are easily met in self-hosted open-source MDM solutions and may be met with commercial solutions when the customer is self-hosting or otherwise controls the DNS. @@ -31,7 +31,7 @@ Apple allows changing most values in profiles delivered by MDM, but the `ServerU 2. Import database records letting Fleet know about the devices to be migrated. 3. Configure controls (profiles, updates, etc.) in Fleet. 4. Install `fleetd` on the devices (through the existing MDM). -5. Update DNS records to point devices to the Fleet server. +5. Update domain (DNS) records to point devices to the Fleet server. 6. Decommission the old server. It is recommended to follow the entire process on a staging/test MDM instance and devices, then repeat for the production instance and devices. diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md index 87188e11ee11..18273f1526c2 100644 --- a/articles/windows-mdm-setup.md +++ b/articles/windows-mdm-setup.md @@ -4,7 +4,7 @@ To control OS settings, updates, and more on Windows hosts follow the manual enrollment instructions. -To use automatic enrollment (aka zero-touch) features on Windows, follow instructions to connect Fleet to Microsoft Azure Active Directory (aka Microsoft Entra). You can further customize zero-touch with Windows Autopilot. +To use automatic enrollment (aka zero-touch) features on Windows, follow instructions to connect Fleet to Microsoft Entra ID. You can further customize zero-touch with Windows Autopilot. ## Manual enrollment @@ -34,7 +34,7 @@ Restart the Fleet server. ### Step 3: Turn on Windows MDM -1. Head to the **Settings > Integrations > Mobile device management (MDM) enrollment** page. +1. Head to the **Settings > Integrations > Mobile device management (MDM)** page. 2. Next to **Turn on Windows MDM** select **Turn on** to navigate to the **Turn on Windows MDM** page. @@ -48,13 +48,13 @@ With Windows MDM turned on, enroll a Windows host to Fleet by installing [Fleet' > Available in Fleet Premium -To automatically enroll Windows workstations when they’re first unboxed and set up by your end users, we will connect Fleet to Microsoft Azure Active Directory (Azure AD). +To automatically enroll Windows workstations when they’re first unboxed and set up by your end users, we will connect Fleet to Microsoft Entra ID. -After you connect Fleet to Azure AD, you can customize the Windows setup experience with [Windows Autopilot](https://learn.microsoft.com/en-us/autopilot/windows-autopilot). +After you connect Fleet to Microsoft Entra ID, you can customize the Windows setup experience with [Windows Autopilot](https://learn.microsoft.com/en-us/autopilot/windows-autopilot). -In order to connect Fleet to Azure AD, the IT admin (you) needs a Microsoft Enterprise Mobility + Security E3 license. +In order to connect Fleet to Microsoft Entra ID, the IT admin (you) needs a Microsoft Enterprise Mobility + Security E3 license. -Each end user who automatically enrolls needs a Microsoft Intune license. +Each end user who automatically enrolls needs a [Microsoft license](https://learn.microsoft.com/en-us/mem/intune/fundamentals/licenses.) ### Step 1: Buy Microsoft licenses @@ -68,9 +68,9 @@ Each end user who automatically enrolls needs a Microsoft Intune license. 5. On the **Enterprise Mobility + Security E3** page, select **Buy** and follow instructions to purchase the license. -6. Find and buy an Intune license. +6. Find and buy a license. -7. Sign in to [Azure portal](https://portal.azure.com). +7. Sign in to [Microsoft Entra ID portal](https://portal.azure.com). 8. At the top of the page search "Users" and select **Users**. @@ -78,15 +78,15 @@ Each end user who automatically enrolls needs a Microsoft Intune license. 10. Select **+ Assignments** and assign yourself the **Enterprise Mobility + Security E3**. Assign the test user the Intune licnese. -### Step 2: Connect Fleet to Azure AD +### Step 2: Connect Fleet to Microsoft Entra ID -For instructions on how to connect Fleet to Azure AD, in the Fleet UI, select the avatar on the right side of the top navigation and select **Settings > Integrations > Automatic enrollment**. Then, next to **Windows automatic enrollment** select **Details**. +For instructions on how to connect Fleet to Microsoft Entra ID, in the Fleet UI, select the avatar on the right side of the top navigation and select **Settings > Integrations > Mobile device management (MDM)**. Then, next to **Windows automatic enrollment** select **Details**. ### Step 3: Test automatic enrollment -Testing automatic enrollment requires creating a test user in Azure AD and a freshly wiped or new Windows workstation. +Testing automatic enrollment requires creating a test user in Microsoft Entra ID and a freshly wiped or new Windows workstation. -1. Sign in to [Azure portal](https://portal.azure.com). +1. Sign in to [Microsoft Entra ID portal](https://portal.azure.com). 2. At the top of the page search "Users" and select **Users**. @@ -124,7 +124,7 @@ Testing automatic enrollment requires creating a test user in Azure AD and a fre ### Step 3: Upload your organization's logo -1. Navigate to [Azure portal](https://portal.azure.com). +1. Navigate to [Microsoft Entra ID portal](https://portal.azure.com). 2. At the top of the page, search for "Microsoft Entra ID", select **Microsoft Entra ID**, and then select **Company branding**. diff --git a/changes/22331-remove-pending-devices b/changes/22331-remove-pending-devices new file mode 100644 index 000000000000..ddd32ef2b0ae --- /dev/null +++ b/changes/22331-remove-pending-devices @@ -0,0 +1 @@ +Remove a pending MDM device if it was deleted from current ABM diff --git a/changes/23174-fix-patch-config-vpp-associations b/changes/23174-fix-patch-config-vpp-associations new file mode 100644 index 000000000000..f8edd86b47ec --- /dev/null +++ b/changes/23174-fix-patch-config-vpp-associations @@ -0,0 +1 @@ +* Fixed bug where `PATCH /api/latest/fleet/config` was incorrectly clearing VPP token<->team associations. diff --git a/changes/23183-opentelemetry b/changes/23183-opentelemetry new file mode 100644 index 000000000000..5dc48a73d24e --- /dev/null +++ b/changes/23183-opentelemetry @@ -0,0 +1,3 @@ +Updated OpenTelemetry libraries to latest versions. This includes the following changes when OpenTelemetry is enabled: +- MySQL spans outside of HTTPS transactions are now logged. +- Renamed MySQL spans to include the query, for easier tracking/debugging. diff --git a/changes/23215-message-spacing b/changes/23215-message-spacing new file mode 100644 index 000000000000..3ad12430b283 --- /dev/null +++ b/changes/23215-message-spacing @@ -0,0 +1 @@ +* Explicitly set line heights on "add profile" messages so they are consistent cross-browser \ No newline at end of file diff --git a/changes/23219 b/changes/23219 new file mode 100644 index 000000000000..ea0982ba0e60 --- /dev/null +++ b/changes/23219 @@ -0,0 +1 @@ +* Make entire rows of the Disk encryption table clickable \ No newline at end of file diff --git a/changes/urf-8 b/changes/urf-8 new file mode 100644 index 000000000000..23095d90726a --- /dev/null +++ b/changes/urf-8 @@ -0,0 +1 @@ +* Fixed incorrect character set header on manual Mac enrollment config download diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 5294e302e88d..a3c310adf3d0 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -307,6 +307,7 @@ func TestAutomationsSchedule(t *testing.T) { } func TestCronVulnerabilitiesCreatesDatabasesPath(t *testing.T) { + t.Skip() // https://github.com/fleetdm/fleet/issues/23258 t.Parallel() ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() @@ -352,10 +353,15 @@ func TestCronVulnerabilitiesCreatesDatabasesPath(t *testing.T) { } // Use schedule to test that the schedule does indeed call cronVulnerabilities. ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium}) + ctx, cancel := context.WithCancel(ctx) lg := kitlog.NewJSONLogger(os.Stdout) s, err := newVulnerabilitiesSchedule(ctx, "test_instance", ds, lg, &config) require.NoError(t, err) s.Start() + t.Cleanup(func() { + cancel() + <-s.Done() + }) assert.Eventually(t, func() bool { info, err := os.Lstat(vulnPath) @@ -660,9 +666,14 @@ func TestCronVulnerabilitiesSkipMkdirIfDisabled(t *testing.T) { // Use schedule to test that the schedule does indeed call cronVulnerabilities. ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium}) + ctx, cancel := context.WithCancel(ctx) s, err := newVulnerabilitiesSchedule(ctx, "test_instance", ds, kitlog.NewNopLogger(), &config) require.NoError(t, err) s.Start() + t.Cleanup(func() { + cancel() + <-s.Done() + }) // Every cron tick is 10 seconds ... here we just wait for a loop interation and assert the vuln // dir. was not created. @@ -1117,7 +1128,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) { fleet.MDMAssetCAKey: {Value: testKeyPEM}, } ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, - _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { + _ sqlx.QueryerContext, + ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) { return assets, nil } ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { diff --git a/cmd/fleetctl/generate.go b/cmd/fleetctl/generate.go index 612740df0d16..255b5a33df07 100644 --- a/cmd/fleetctl/generate.go +++ b/cmd/fleetctl/generate.go @@ -49,24 +49,24 @@ func generateMDMAppleCommand() *cli.Command { // before printing the CSR output message. client, err := clientFromCLI(c) if err != nil { - fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err) + fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s\n", err) return ErrGeneric } csr, err := client.RequestAppleCSR() if err != nil { - fmt.Fprintf(c.App.ErrWriter, "requesting APNs CSR: %s", err) + fmt.Fprintf(c.App.ErrWriter, "requesting APNs CSR: %s\n", err) return ErrGeneric } if err := os.WriteFile(csrPath, csr, defaultFileMode); err != nil { - fmt.Fprintf(c.App.ErrWriter, "write CSR: %s", err) + fmt.Fprintf(c.App.ErrWriter, "write CSR: %s\n", err) return ErrGeneric } appCfg, err := client.GetAppConfig() if err != nil { - fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err) + fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s\n", err) return ErrGeneric } diff --git a/cmd/fleetctl/gitops.go b/cmd/fleetctl/gitops.go index 2382840f67fa..04ca1469c0bc 100644 --- a/cmd/fleetctl/gitops.go +++ b/cmd/fleetctl/gitops.go @@ -86,8 +86,13 @@ func gitopsCommand() *cli.Command { noTeamControls spec.Controls noTeamPresent bool ) + isPremium := appConfig.License.IsPremium() for _, flFilename := range flFilenames.Value() { if filepath.Base(flFilename) == "no-team.yml" { + if !isPremium { + // Message is printed in the next flFilenames loop to avoid printing it multiple times + break + } baseDir := filepath.Dir(flFilename) config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, func(format string, a ...interface{}) {}) if err != nil { @@ -148,13 +153,16 @@ func gitopsCommand() *cli.Command { if !config.Controls.Set() { config.Controls = noTeamControls } + } else if !isPremium { + logf("[!] skipping team config %s since teams are only supported for premium Fleet users\n", flFilename) + continue } // Special handling for tokens is required because they link to teams (by // name.) Because teams can be created/deleted during the same gitops run, we // grab some information to help us determine allowed/restricted actions and // when to perform the associations. - if isGlobalConfig && totalFilenames > 1 && !(totalFilenames == 2 && noTeamPresent) { + if isGlobalConfig && totalFilenames > 1 && !(totalFilenames == 2 && noTeamPresent) && isPremium { abmTeams, hasMissingABMTeam, usesLegacyABMConfig, err = checkABMTeamAssignments(config, fleetClient) if err != nil { return err diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index fefe4c3153d2..3dc253804192 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -1020,6 +1020,50 @@ software: assert.Equal(t, filepath.Base(tmpFile.Name()), *savedTeam.Filename) } +func createFakeITunesAndVPPServices(t *testing.T) { + config := &appleVPPConfigSrvConf{ + Assets: []vpp.Asset{ + { + AdamID: "1", + PricingParam: "STDQ", + AvailableCount: 12, + }, + { + AdamID: "2", + PricingParam: "STDQ", + AvailableCount: 3, + }, + }, + SerialNumbers: []string{"123", "456"}, + } + startVPPApplyServer(t, config) + + appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // a map of apps we can respond with + db := map[string]string{ + // macos app + "1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`, + // macos, ios, ipados app + "2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2, + "supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`, + // ipados app + "3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3, + "supportedDevices": ["iPadAir-iPadAir"] }`, + } + + adamIDString := r.URL.Query().Get("id") + adamIDs := strings.Split(adamIDString, ",") + + var objs []string + for _, a := range adamIDs { + objs = append(objs, db[a]) + } + + _, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ",")))) + })) + t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL) +} + func TestGitOpsBasicGlobalAndTeam(t *testing.T) { // Cannot run t.Parallel() because it sets environment variables license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)} @@ -1033,7 +1077,8 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { // Mock appConfig savedAppConfig := &fleet.AppConfig{} ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { - return &fleet.AppConfig{}, nil + appConfig := savedAppConfig.Copy() + return appConfig, nil } ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error { savedAppConfig = config @@ -1104,6 +1149,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { return nil, nil, nil } ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) { + if savedTeam != nil { + return []*fleet.Team{savedTeam}, nil + } return nil, nil } ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } @@ -1158,8 +1206,12 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { return nil } + vppToken := &fleet.VPPTokenDB{ + Location: "Foobar", + RenewDate: time.Now().Add(24 * 365 * time.Hour), + } ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) { - return []*fleet.VPPTokenDB{}, nil + return []*fleet.VPPTokenDB{vppToken}, nil } ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { @@ -1169,11 +1221,38 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { return nil } + ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) { + var teamsSummary []*fleet.TeamSummary + if savedTeam != nil { + teamsSummary = append(teamsSummary, &fleet.TeamSummary{ + ID: savedTeam.ID, + Name: savedTeam.Name, + Description: savedTeam.Description, + }) + } + return teamsSummary, nil + } + + ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) { + if teamID != nil && *teamID == savedTeam.ID { + return vppToken, nil + } + return nil, ¬FoundError{} + } + + ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) { + return vppToken, nil + } + + createFakeITunesAndVPPServices(t) + globalFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) t.Setenv("FLEET_SERVER_URL", fleetServerURL) t.Setenv("ORG_NAME", orgName) + t.Setenv("TEST_TEAM_NAME", teamName) + t.Setenv("TEST_SECRET", secret) _, err = globalFile.WriteString( ` @@ -1189,6 +1268,11 @@ org_settings: org_logo_url: "" org_logo_url_light_background: "" org_name: ${ORG_NAME} + mdm: + volume_purchasing_program: + - location: Foobar + teams: + - "${TEST_TEAM_NAME}" secrets: [{"secret":"globalSecret"}] software: `, @@ -1198,9 +1282,6 @@ software: teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - t.Setenv("TEST_TEAM_NAME", teamName) - t.Setenv("TEST_SECRET", secret) - _, err = teamFile.WriteString( ` controls: @@ -1211,6 +1292,8 @@ name: ${TEST_TEAM_NAME} team_settings: secrets: [{"secret":"${TEST_SECRET}"}] software: + app_store_apps: + - app_store_id: '1' `, ) require.NoError(t, err) @@ -1246,12 +1329,18 @@ software: require.Error(t, err) assert.ErrorContains(t, err, "duplicate enroll secret found") + ds.GetVPPTokenByTeamIDFuncInvoked = false + // Dry run _ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"}) assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty") + // Dry run should not attempt to get the VPP token when applying VPP apps (it may not exist). + require.False(t, ds.GetVPPTokenByTeamIDFuncInvoked) + ds.ListTeamsFuncInvoked = false + // Dry run, deleting other teams - assert.False(t, ds.ListTeamsFuncInvoked) + savedAppConfig = &fleet.AppConfig{} _ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run", "--delete-other-teams"}) assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty") assert.True(t, ds.ListTeamsFuncInvoked) @@ -1266,6 +1355,12 @@ software: require.Len(t, enrolledTeamSecrets, 1) assert.Equal(t, secret, enrolledTeamSecrets[0].Secret) + // Dry run again (after team was created by real run) + ds.GetVPPTokenByTeamIDFuncInvoked = false + _ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"}) + // Dry run should not attempt to get the VPP token when applying VPP apps (it may not exist). + require.False(t, ds.GetVPPTokenByTeamIDFuncInvoked) + // Now, set up a team to delete teamToDeleteID := uint(999) teamToDelete := &fleet.Team{ diff --git a/docs/Configuration/fleet-server-configuration.md b/docs/Configuration/fleet-server-configuration.md index 89fa9072ee40..c64dc6f97d10 100644 --- a/docs/Configuration/fleet-server-configuration.md +++ b/docs/Configuration/fleet-server-configuration.md @@ -757,26 +757,6 @@ The license key provided to Fleet customers which provides access to Fleet Premi key: foobar ``` -##### license_enforce_host_limit - -Whether Fleet should enforce the host limit of the license, if true, attempting to enroll new hosts when the limit is reached will fail. - -- Default value: `false` -- Environment variable: `FLEET_LICENSE_ENFORCE_HOST_LIMIT` -- Config file format: - ```yaml - license: - enforce_host_limit: true - ``` - -##### Example YAML - -```yaml -license: - key: foobar - enforce_host_limit: false -``` - #### Session ##### session_key_size diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md index 15271d1cc719..25b80cdc7463 100644 --- a/docs/Configuration/yaml-files.md +++ b/docs/Configuration/yaml-files.md @@ -4,20 +4,6 @@ Use Fleet's best practice GitOps workflow to manage your computers as code. To learn how to set up a GitOps workflow see the [Fleet GitOps repo](https://github.com/fleetdm/fleet-gitops). -## File structure - -- `default.yml` - File where you define the queries, policies and agent options for all hosts. If you're using Fleet Premium, this file updates queries and policies that run on all hosts ("All teams"). -- `teams/no-team.yml` - File where you define the policies, controls, and software for hosts on "No team". Available in Fleet Premium. -- `teams/` - Folder where you define your teams in Fleet. These `teams/team-name.yml` files define the controls, queries, policies, software, and agent options for hosts assigned to the specified team. Available in Fleet Premium. -- `lib/` - Folder where you define policies, queries, configuration profiles, scripts, and agent options. These files can be referenced in top level keys in the `default.yml` file and the files in the `teams/` folder. -- `.github/workflows/workflow.yml` - The GitHub workflow file where you can add [environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow). - -The following files are responsible for running the GitHub action or GitLab CI/CD. Most users don't need to edit these files. -- `gitops.sh` - The bash script that applies the latest configuration to Fleet. This script is used in the GitHub action file. -- `.github/gitops-action/action.yml` - The GitHub action that runs `gitops.sh`. This action is used in the GitHub workflow file. It can also be used in other workflows. -- `.gitlab-ci.yml` - The GitLab CI/CD file that applies the latest configuration to Fleet. - -## Configuration options The following are the required keys in the `default.yml` and any `teams/team-name.yml` files: @@ -31,14 +17,7 @@ org_settings: # Only default.yml team_settings: # Only teams/team-name.yml ``` -- [policies](#policies) -- [queries](#queries) -- [agent_options](#agent-options) -- [controls](#controls) -- [software](#software) -- [org_settings and team_settings](#org-settings-and-team-settings) - -### policies +## policies Policies can be specified inline in your `default.yml`, `teams/team-name.yml`, or `teams/no-team.yml` files. They can also be specified in separate files in your `lib/` folder. @@ -48,13 +27,13 @@ Policies defined in `teams/no-team.yml` run on hosts that belong to "No team". > Policies that run automations to install software or run scripts must be defined in `teams/no-team.yml` to run on hosts that belong to "No team". -#### Options +### Options For possible options, see the parameters for the [Add policy API endpoint](https://fleetdm.com/docs/rest-api/rest-api#add-policy). -#### Example +### Example -##### Inline +#### Inline `default.yml`, `teams/team-name.yml`, or `teams/no-team.yml` @@ -69,7 +48,7 @@ policies: calendar_event_enabled: false ``` -##### Separate file +#### Separate file `lib/policies-name.policies.yml` @@ -107,19 +86,19 @@ policies: # path is relative to default.yml or teams/team-name.yml ``` -### queries +## queries Queries can be specified inline in your `default.yml` file or `teams/team-name.yml` files. They can also be specified in separate files in your `lib/` folder. Note that the `team_id` option isn't supported in GitOps. -#### Options +### Options For possible options, see the parameters for the [Create query API endpoint](https://fleetdm.com/docs/rest-api/rest-api#create-query). -#### Example +### Example -##### Inline +#### Inline `default.yml` or `teams/team-name.yml` @@ -134,7 +113,7 @@ queries: automations_enabled: false ``` -##### Separate file +#### Separate file `lib/queries-name.queries.yml` @@ -163,15 +142,15 @@ queries: # path is relative to default.yml or teams/team-name.yml ``` -### agent_options +## agent_options Agent options can be specified inline in your `default.yml` file or `teams/team-name.yml` files. They can also be specified in separate files in your `lib/` folder. See "[Agent configuration](https://fleetdm.com/docs/configuration/agent-configuration)" to find all possible options. -#### Example +### Example -##### Inline +#### Inline `default.yml` or `teams/team-name.yml` @@ -192,7 +171,7 @@ agent_options: pack_delimiter: / ``` -##### Separate file +#### Separate file `lib/agent-options.yml` @@ -222,7 +201,7 @@ queries: # path is relative to default.yml or teams/team-name.yml ``` -### controls +## controls The `controls` section allows you to configure scripts and device management (MDM) features in Fleet. @@ -232,7 +211,7 @@ Controls for hosts that are in "No team" can be defined in `default.yml` or in ` - `windows_enabled_and_configured` specifies whether or not to turn on Windows MDM features (default: `false`). Can only be configured for all teams (`default.yml`). - `enable_disk_encryption` specifies whether or not to enforce disk encryption on macOS and Windows hosts (default: `false`). -##### Example +#### Example ```yaml controls: @@ -270,27 +249,27 @@ controls: # paths are relative to default.yml or teams/team-name.yml ``` -#### macos_updates +### macos_updates - `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`). - `minimum_version` specifies the minimum required macOS version (default: `""`). -#### windows_updates +### windows_updates - `deadline_days` (default: null) - `grace_period_days` (default: null) -#### ios_updates +### ios_updates - `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`). - `minimum_version` specifies the minimum required iOS version (default: `""`). -#### ipados_updates +### ipados_updates - `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`). - `minimum_version` specifies the minimum required iPadOS version (default: `""`). -#### macos_settings and windows_settings +### macos_settings and windows_settings - `macos_settings.custom_settings` is a list of paths to macOS configuration profiles (.mobileconfig) or declaration profiles (.json). - `windows_settings.custom_settings` is a list of paths to Windows configuration profiles (.xml). @@ -299,7 +278,7 @@ Fleet supports adding [GitHub environment variables](https://docs.github.com/en/ Use `labels_include_all` to only apply (scope) profiles to hosts that have all those labels or `labels_exclude_any` to apply profiles to hosts that don't have any of those labels. -#### macos_setup +### macos_setup The `macos_setup` section lets you control the out-of-the-box macOS [setup experience](https://fleetdm.com/guides/macos-setup-experience) for hosts that use Automated Device Enrollment (ADE). @@ -307,7 +286,7 @@ The `macos_setup` section lets you control the out-of-the-box macOS [setup exper - `enable_end_user_authentication` specifies whether or not to require end user authentication when the user first sets up their macOS host. - `macos_setup_assistant` is a path to a custom automatic enrollment (ADE) profile (.json). -#### macos_migration +### macos_migration The `macos_migration` section lets you control the [end user migration workflow](https://fleetdm.com/docs/using-fleet/mdm-migration-guide#end-user-workflow) for macOS hosts that enrolled to your old MDM solution. @@ -317,7 +296,7 @@ The `macos_migration` section lets you control the [end user migration workflow] Can only be configure for all teams (`default.yml`). -### software +## software > **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows. @@ -328,9 +307,9 @@ Software can also be specified in separate files in your `lib/` folder. - `packages` is a list of software packages (.pkg, .msi, .exe, .rpm, or .deb) and software specific options. - `app_store_apps` is a list of Apple App Store apps. -#### Example +### Example -##### Inline +#### Inline ```yaml software: @@ -348,7 +327,7 @@ software: - app_store_id: '1091189122' ``` -##### packages +#### packages - `url` specifies the URL at which the software is located. Fleet will download the software and upload it to S3 (default: `""`). - `install_script.path` specifies the command Fleet will run on hosts to install software. The [default script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) is dependent on the software type (i.e. .pkg). @@ -356,7 +335,7 @@ software: - `post_install_script.path` is the script Fleet will run on hosts after intalling software (default: `""`). - `self_service` specifies whether or not end users can install from **Fleet Desktop > Self-service**. -##### app_store_apps +#### app_store_apps - `app_store_id` is the ID of the Apple App Store app. You can find this at the end of the app's App Store URL. For example, "Bear - Markdown Notes" URL is "https://apps.apple.com/us/app/bear-markdown-notes/id1016366447" and the `app_store_id` is `1016366447`. @@ -364,7 +343,7 @@ software: `self_service` only applies to macOS, and is ignored for other platforms. For example, if the app is supported on macOS, iOS, and iPadOS, and `self_service` is set to `true`, it will be self-service on macOS workstations but not iPhones or iPads. -##### Separate file +#### Separate file `lib/software-name.package.yml`: @@ -393,16 +372,16 @@ software: # path is relative to default.yml or teams/team-name.yml ``` -### org_settings and team_settings +## org_settings and team_settings -#### features +### features The `features` section of the configuration YAML lets you define what predefined queries are sent to the hosts and later on processed by Fleet for different functionalities. - `additional_queries` adds extra host details. This information will be updated at the same time as other host details and is returned by the API when host objects are returned (default: empty). - `enable_host_users` specifies whether or not Fleet collects user data from hosts (default: `true`). - `enable_software_inventory` specifies whether or not Fleet collects softwre inventory from hosts (default: `true`). -##### Example +#### Example ```yaml org_settings: @@ -414,13 +393,13 @@ org_settings: enable_software_inventory: true ``` -#### fleet_desktop +### fleet_desktop Direct end users to a custom URL when they select **Transparency** in the Fleet Desktop dropdown (default: [https://fleetdm.com/transparency](https://fleetdm.com/transparency)). Can only be configured for all teams (`org_settings`). -##### Example +#### Example ```yaml org_settings: @@ -428,13 +407,13 @@ org_settings: transparency_url: "https://example.org/transparency" ``` -#### host_expiry_settings +### host_expiry_settings The `host_expiry_settings` section lets you define if and when hosts should be automatically deleted from Fleet if they have not checked in. - `host_expiry_enabled` (default: `false`) - `host_expiry_window` if a host has not communicated with Fleet in the specified number of days, it will be removed. Must be > `0` when host expiry is enabled (default: `0`). -##### Example +#### Example ```yaml org_settings: @@ -443,7 +422,7 @@ org_settings: host_expiry_window: 10 ``` -#### org_info +### org_info - `name` is the name of your organization (default: `""`) - `logo_url` is a public URL of the logo for your organization (default: Fleet logo). @@ -452,7 +431,7 @@ org_settings: Can only be configured for all teams (`org_settings`). -##### Example +#### Example ```yaml org_settings: @@ -463,11 +442,11 @@ org_settings: contact_url: https://fleetdm.com/company/contact ``` -#### secrets +### secrets The `secrets` section defines the valid secrets that hosts can use to enroll to Fleet. Supply one of these secrets when generating the fleetd agent you'll use to enroll hosts. Learn more [here](https://fleetdm.com/docs/using-fleet/enroll-hosts). -##### Example +#### Example ```yaml org_settings: @@ -475,18 +454,18 @@ org_settings: - $ENROLL_SECRET ``` -#### server_settings +### server_settings - `enable_analytics` specifies whether or not to enable Fleet's [usage statistics](https://fleetdm.com/docs/using-fleet/usage-statistics) (default: `true`). - `live_query_disabled` disables the ability to run live queries (ad hoc queries executed via the UI or fleetctl) (default: `false`). - `query_reports_disabled` disables query reports and deletes existing repors (default: `false`). - `query_report_cap` sets the maximum number of results to store per query report before the report is clipped. If increasing this cap, we recommend enabling reports for one query at time and monitoring your infrastructure. (Default: `1000`) -- `scripts_disabled` blocks access to run scripts. Scripts may still be added in the UI and CLI (defaul: `false`). +- `scripts_disabled` blocks access to run scripts. Scripts may still be added in the UI and CLI (default: `false`). - `server_url` is the base URL of the Fleet instance (default: provided during Fleet setup) Can only be configured for all teams (`org_settings`). -##### Example +#### Example ```yaml org_settings: @@ -499,7 +478,7 @@ org_settings: ``` -#### sso_settings +### sso_settings The `sso_settings` section lets you define single sign-on (SSO) settings. Learn more about SSO in Fleet [here](https://fleetdm.com/docs/deploying/configuration#configuring-single-sign-on-sso). @@ -513,7 +492,7 @@ The `sso_settings` section lets you define single sign-on (SSO) settings. Learn Can only be configured for all teams (`org_settings`). -##### Example +#### Example ```yaml org_settings: @@ -526,11 +505,11 @@ org_settings: enable_sso_idp_login: true ``` -#### integrations +### integrations The `integrations` section lets you define calendar events and ticket settings for failing policy and vulnerablity automations. Learn more about automations in Fleet [here](https://fleetdm.com/docs/using-fleet/automations). -##### Example +#### Example ```yaml org_settings: @@ -552,37 +531,37 @@ org_settings: For secrets, you can add [GitHub environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow) -##### google_calendar +#### google_calendar - `api_key_json` is the contents of the JSON file downloaded when you create your Google Workspace service account API key (default: `""`). - `domain` is the primary domain used to identify your end user's work calendar (default: `""`). -##### jira +#### jira - `url` is the URL of your Jira (default: `""`) - `username` is the username of your Jira account (default: `""`). - `api_token` is the Jira API token (default: `""`). - `project_key` is the project key location in your Jira project's URL. For example, in "jira.example.com/projects/EXMPL," "EXMPL" is the project key (default: `""`). -##### zendesk +#### zendesk - `url` is the URL of your Zendesk (default: `""`) - `username` is the username of your Zendesk account (default: `""`). - `api_token` is the Zendesk API token (default: `""`). - `group_id`is found by selecting **Admin > People > Groups** in Zendesk. Find your group and select it. The group ID will appear in the search field. -#### webhook_settings +### webhook_settings The `webhook_settings` section lets you define webhook settings for failing policy, vulnerability, and host status automations. Learn more about automations in Fleet [here](https://fleetdm.com/docs/using-fleet/automations). -##### failing_policies_webhook +#### failing_policies_webhook - `enable_failing_policies_webhook` (default: `false`) - `destination_url` is the URL to `POST` to when the condition for the webhook triggers (default: `""`). - `policy_ids` is the list of policies that will trigger a webhook. - `host_batch_size` is the maximum number of hosts to batch in each webhook. A value of `0` means no batching (default: `0`). -##### Example +#### Example ```yaml org_settings: @@ -597,14 +576,14 @@ org_settings: - 3 ``` -##### host_status_webhook +#### host_status_webhook - `enable_host_status_webhook` (default: `false`) - `destination_url` is the URL to `POST` to when the condition for the webhook triggers (default: `""`). - `days_count` is the number of days that hosts need to be offline to count as part of the percentage (default: `0`). - `host_percentage` is the percentage of hosts that need to be offline to trigger the webhook. (default: `0`). -##### Example +#### Example ```yaml org_settings: @@ -616,14 +595,14 @@ org_settings: host_percentage: 25 ``` -##### vulnerabilities_webhook +#### vulnerabilities_webhook - `enable_vulnerabilities_webhook` (default: `false`) - `destination_url` is the URL to `POST` to when the condition for the webhook triggers (default: `""`). - `days_count` is the number of days that hosts need to be offline to count as part of the percentage (default: `0`). - `host_batch_size` is the maximum number of hosts to batch in each webhook. A value of `0` means no batching (default: `0`). -##### Example +#### Example ```yaml org_settings: @@ -636,16 +615,16 @@ org_settings: Can only be configured for all teams (`org_settings`). -#### mdm +### mdm -##### apple_business_manager +#### apple_business_manager - `organization_name` is the organization name associated with the Apple Business Manager account. - `macos_team` is the team where macOS hosts are automatically added when they appear in Apple Business Manager. - `ios_team` is the the team where iOS hosts are automatically added when they appear in Apple Business Manager. - `ipados_team` is the team where iPadOS hosts are automatically added when they appear in Apple Business Manager. -##### Example +#### Example ```yaml org_settings: @@ -659,12 +638,12 @@ org_settings: > Apple Business Manager settings can only be configured for all teams (`org_settings`). -##### volume_purchasing_program +#### volume_purchasing_program - `location` is the name of the location in the Apple Business Manager account. - `teams` is a list of team names. If you choose specific teams, App Store apps in this VPP account will only be available to install on hosts in these teams. If not specified, App Store apps are available to install on hosts in all teams. -##### Example +#### Example ```yaml org_settings: @@ -680,7 +659,7 @@ org_settings: Can only be configured for all teams (`org_settings`). -##### end_user_authentication +#### end_user_authentication The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula). @@ -693,7 +672,7 @@ Once the IdP settings are configured, you can use the [`controls.macos_setup.ena Can only be configured for all teams (`org_settings`). -##### end_user_authentication +#### end_user_authentication The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula). diff --git a/docs/Contributing/Configuration-for-contributors.md b/docs/Contributing/Configuration-for-contributors.md index 7e3c6798ce4d..ce3dc4e574ba 100644 --- a/docs/Contributing/Configuration-for-contributors.md +++ b/docs/Contributing/Configuration-for-contributors.md @@ -156,6 +156,26 @@ This is the content of the PEM-encoded private key for the Apple Business Manage -----END RSA PRIVATE KEY----- ``` +##### license.enforce_host_limit + +Whether Fleet should enforce the host limit of the license, if true, attempting to enroll new hosts when the limit is reached will fail. + +- Default value: `false` +- Environment variable: `FLEET_LICENSE_ENFORCE_HOST_LIMIT` +- Config file format: + ```yaml + license: + enforce_host_limit: true + ``` + +##### Example YAML + +```yaml +license: + key: foobar + enforce_host_limit: false +``` + ## Environment variables ### FLEET_ENABLE_POST_CLIENT_DEBUG_ERRORS diff --git a/docs/Deploy/deploy-fleet.md b/docs/Deploy/deploy-fleet.md index 672e6283b09c..b1771569a242 100644 --- a/docs/Deploy/deploy-fleet.md +++ b/docs/Deploy/deploy-fleet.md @@ -33,7 +33,7 @@ Render is a cloud hosting service that makes it easy to get up and running fast, - A Render account with payment information. ->The Fleet Render Blueprint will provision a web service, a MySQL database, and a Redis in-memory data store. Each service requires Render's standard plan at a cost of **$7/month** each, totaling **$21/month**. +>The Fleet Render Blueprint will provision a web service, a MySQL database, and a Redis in-memory data store. At current pricing this will total **$62/month**. ### Instructions diff --git a/docs/Get started/FAQ.md b/docs/Get started/FAQ.md index 5c7feeacbdb0..706f32c353cb 100644 --- a/docs/Get started/FAQ.md +++ b/docs/Get started/FAQ.md @@ -73,13 +73,13 @@ We test each browser on Windows whenever possible, because our engineering team Fleet supports the following operating system versions on hosts. -| OS | Supported version(s) | -| :------ | :------------------------------------- | -| macOS | 13+ (Ventura) | -| iOS | 17+ | -| Windows | Pro and Enterprise 10+, Server 2012+ | -| Linux | CentOS 7.1+, Ubuntu 20.04+, Fedora 38+ | -| ChromeOS | 112.0.5615.134+ | +| OS | Supported version(s) | +| :--------- | :-------------------------------------- | +| macOS | 13+ (Ventura) | +| iOS/iPadOS | 17+ | +| Windows | Pro and Enterprise 10+, Server 2012+ | +| Linux | CentOS 7.1+, Ubuntu 20.04+, Fedora 38+ | +| ChromeOS | 112.0.5615.134+ | While Fleet may still function partially or fully with OS versions older than those above, Fleet does not actively test against unsupported versions and does not pursue bugs on them. diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 6dc53f0289f8..efa44a27066d 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -3686,6 +3686,8 @@ _Available in Fleet Premium_ ### Turn off MDM for a host +Turns off MDM for the specified macOS host. + `DELETE /api/v1/fleet/hosts/:id/mdm` #### Parameters @@ -5865,7 +5867,7 @@ Learn more about OTA profiles [here](https://developer.apple.com/library/archive ```http Content-Length: 542 - Content-Type: application/x-apple-aspen-config; charset=urf-8 + Content-Type: application/x-apple-aspen-config; charset=utf-8 Content-Disposition: attachment;filename="fleet-mdm-enrollment-profile.mobileconfig" X-Content-Type-Options: nosniff ``` @@ -9293,7 +9295,7 @@ Add a package (.pkg, .msi, .exe, .deb, .rpm) to install on macOS, Windows, or Li | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | -| software | file | form | **Required**. Installer package file. Supported packages are PKG, MSI, EXE, DEB, and RPM. | +| software | file | form | **Required**. Installer package file. Supported packages are .pkg, .msi, .exe, .deb, and .rpm. | | team_id | integer | form | **Required**. The team ID. Adds a software package to the specified team. | | install_script | string | form | Script that Fleet runs to install software. If not specified Fleet runs [default install script](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. | | pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, Fleet won't proceed to install. | @@ -9354,7 +9356,7 @@ Update a package to install on macOS, Windows, or Linux (Ubuntu) hosts. | Name | Type | In | Description | | ---- | ------- | ---- | -------------------------------------------- | -| software | file | form | Installer package file. Supported packages are PKG, MSI, EXE, DEB, and RPM. | +| software | file | form | Installer package file. Supported packages are .pkg, .msi, .exe, .deb, and .rpm. | | team_id | integer | form | **Required**. The team ID. Updates a software package in the specified team. | | install_script | string | form | Command that Fleet runs to install software. If not specified Fleet runs the [default install command](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. | | pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, the package will not be installed. | diff --git a/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js b/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js index a840b666d706..abe14930ea66 100644 --- a/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js +++ b/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js @@ -61,7 +61,12 @@ module.exports = { // let { Readable } = require('stream'); let axios = require('axios'); // Cast the strings in the newTeamIds array to numbers. - newTeamIds = newTeamIds.map(Number); + if(newTeamIds){ + newTeamIds = newTeamIds.map(Number); + } else { + newTeamIds = []; + } + let currentSoftwareTeamIds = _.pluck(software.teams, 'fleetApid'); // If the teams have changed, or a new installer package was provided, we'll need to upload the package to an s3 bucket to deploy it to other teams. if(_.xor(newTeamIds, currentSoftwareTeamIds).length !== 0 || newSoftware) { @@ -248,6 +253,28 @@ module.exports = { }); // console.timeEnd(`transfering ${software.name} to fleet instance for team id ${teamApid}`); });// After every team the software is currently deployed to. + } else if(preInstallQuery !== software.preInstallQuery || + installScript !== software.installScript || + postInstallScript !== software.postInstallScript || + uninstallScript !== software.uninstallScript) { + await sails.helpers.flow.simultaneouslyForEach(unchangedTeamIds, async (teamApid)=>{ + await sails.helpers.http.sendHttpRequest.with({ + method: 'PATCH', + baseUrl: sails.config.custom.fleetBaseUrl, + url: `/api/v1/fleet/software/titles/${software.fleetApid}/package?team_id=${teamApid}`, + enctype: 'multipart/form-data', + headers: { + Authorization: `Bearer ${sails.config.custom.fleetApiToken}` + }, + body: { + team_id: teamApid, // eslint-disable-line camelcase + pre_install_query: preInstallQuery, // eslint-disable-line camelcase + install_script: installScript, // eslint-disable-line camelcase + post_install_script: postInstallScript, // eslint-disable-line camelcase + uninstall_script: uninstallScript, // eslint-disable-line camelcase + } + }); + }); } // Now delete the software from teams it was removed from. for(let team of removedTeams) { @@ -275,7 +302,11 @@ module.exports = { uploadFd: softwareFd, uploadMime: softwareMime, name: softwareName, - platform: _.endsWith(softwareName, '.deb') ? 'Linux' : _.endsWith(softwareName, '.pkg') ? 'macOS' : 'Windows', + platform: software.platform, + postInstallScript, + preInstallQuery, + installScript, + uninstallScript, }); } else { // Save the information about the undeployed software in the app's DB. @@ -289,17 +320,17 @@ module.exports = { installScript, uninstallScript, }); - // Now delete the software on the Fleet instance. - for(let team of software.teams) { - await sails.helpers.http.sendHttpRequest.with({ - method: 'DELETE', - baseUrl: sails.config.custom.fleetBaseUrl, - url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`, - headers: { - Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, - } - }); - } + } + // Now delete the software on the Fleet instance. + for(let team of software.teams) { + await sails.helpers.http.sendHttpRequest.with({ + method: 'DELETE', + baseUrl: sails.config.custom.fleetBaseUrl, + url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`, + headers: { + Authorization: `Bearer ${sails.config.custom.fleetApiToken}`, + } + }); } } else { @@ -308,6 +339,10 @@ module.exports = { name: softwareName, uploadMime: softwareMime, uploadFd: softwareFd, + preInstallQuery, + installScript, + postInstallScript, + uninstallScript, }); // console.log('removing old stored copy of '+softwareName); await sails.rm(sails.config.uploads.prefixForFileDeletion+software.uploadFd); diff --git a/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js b/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js index 094b21866c6d..e3a3ea2ef07b 100644 --- a/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js +++ b/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js @@ -47,7 +47,7 @@ parasails.registerComponent('aceEditor', { // ╩ ╩ ╩ ╩ ╩╩═╝ template: `
-
{{value}}
+
{{value}}
`, diff --git a/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js b/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js index 3eb72e810932..a9c50ef8a279 100644 --- a/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js +++ b/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js @@ -60,13 +60,13 @@ parasails.registerPage('software', { } }, clickOpenEditModal: async function(software) { - this.softwareToEdit = _.clone(software); + this.softwareToEdit = _.cloneDeep(software); this.formData.newTeamIds = _.pluck(this.softwareToEdit.teams, 'fleetApid'); this.formData.software = software; - this.formData.preInstallQuery = software.preInstallQuery; - this.formData.installScript = software.installScript; - this.formData.postInstallScript = software.postInstallScript; - this.formData.uninstallScript = software.uninstallScript; + this.formData.preInstallQuery = this.softwareToEdit.preInstallQuery; + this.formData.installScript = this.softwareToEdit.installScript; + this.formData.postInstallScript = this.softwareToEdit.postInstallScript; + this.formData.uninstallScript = this.softwareToEdit.uninstallScript; this.modal = 'edit-software'; }, clickOpenDeleteModal: async function(software) { @@ -108,9 +108,11 @@ parasails.registerPage('software', { this.softwareToDisplay = softwareOnThisTeam; }, handleSubmittingEditSoftwareForm: async function() { - let argins = _.clone(this.formData); - if(argins.newTeamIds === [undefined]){ - argins.newTeamIds = []; + let argins = _.cloneDeep(this.formData); + if(argins.newTeamIds[0] === undefined){ + argins.newTeamIds = undefined; + } else { + argins.newTeamIds = _.uniq(argins.newTeamIds); } await Cloud.editSoftware.with(argins); if(!this.cloudError) { diff --git a/ee/bulk-operations-dashboard/package.json b/ee/bulk-operations-dashboard/package.json index b19e2f9a18f3..f36c7a9e5073 100644 --- a/ee/bulk-operations-dashboard/package.json +++ b/ee/bulk-operations-dashboard/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "eslint": "5.16.0", - "grunt": "1.0.4", + "grunt": "1.5.3", "htmlhint": "0.11.0", "lesshint": "6.3.6", "sails-hook-grunt": "^5.0.0" diff --git a/ee/bulk-operations-dashboard/views/pages/software/software.ejs b/ee/bulk-operations-dashboard/views/pages/software/software.ejs index a4282aae0455..f9ab497303cc 100644 --- a/ee/bulk-operations-dashboard/views/pages/software/software.ejs +++ b/ee/bulk-operations-dashboard/views/pages/software/software.ejs @@ -187,7 +187,7 @@
Please select the teams you want to deploy this software to.
- A software with the same name as the uploaded software already exists on one or more of the selected teams. + A software with the same name as the uploaded software already exists on one or more of the selected teams.
Cancel diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go index dc1c1c461e74..453bdcee132d 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -86,6 +86,12 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, }}...) } + if dryRun { + // On dry runs return early because the VPP token might not exist yet + // and we don't want to apply the VPP apps. + return nil + } + var vppAppTeams []fleet.VPPAppTeam // Don't check for token if we're only disassociating assets if len(payloads) > 0 { @@ -136,35 +142,33 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string, } } - if !dryRun { - if len(vppAppTeams) > 0 { - apps, err := getVPPAppsMetadata(ctx, vppAppTeams) - if err != nil { - return ctxerr.Wrap(ctx, err, "refreshing VPP app metadata") - } - if len(apps) == 0 { - return fleet.NewInvalidArgumentError("app_store_apps", - "no valid apps found matching the provided app store IDs and platforms") - } - - if err := svc.ds.BatchInsertVPPApps(ctx, apps); err != nil { - return ctxerr.Wrap(ctx, err, "inserting vpp app metadata") - } - // Filter out the apps with invalid platforms - if len(apps) != len(vppAppTeams) { - vppAppTeams = make([]fleet.VPPAppTeam, 0, len(apps)) - for _, app := range apps { - vppAppTeams = append(vppAppTeams, app.VPPAppTeam) - } - } + if len(vppAppTeams) > 0 { + apps, err := getVPPAppsMetadata(ctx, vppAppTeams) + if err != nil { + return ctxerr.Wrap(ctx, err, "refreshing VPP app metadata") + } + if len(apps) == 0 { + return fleet.NewInvalidArgumentError("app_store_apps", + "no valid apps found matching the provided app store IDs and platforms") + } + if err := svc.ds.BatchInsertVPPApps(ctx, apps); err != nil { + return ctxerr.Wrap(ctx, err, "inserting vpp app metadata") } - if err := svc.ds.SetTeamVPPApps(ctx, &team.ID, vppAppTeams); err != nil { - if errors.Is(err, sql.ErrNoRows) { - return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity) + // Filter out the apps with invalid platforms + if len(apps) != len(vppAppTeams) { + vppAppTeams = make([]fleet.VPPAppTeam, 0, len(apps)) + for _, app := range apps { + vppAppTeams = append(vppAppTeams, app.VPPAppTeam) } - return ctxerr.Wrap(ctx, err, "set team vpp assets") } + + } + if err := svc.ds.SetTeamVPPApps(ctx, &team.ID, vppAppTeams); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity) + } + return ctxerr.Wrap(ctx, err, "set team vpp assets") } return nil diff --git a/frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx b/frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx new file mode 100644 index 000000000000..a1f7a71c14c5 --- /dev/null +++ b/frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { screen } from "@testing-library/react"; +import { renderWithSetup } from "test/test-utils"; + +import ActionsDropdown from "./ActionsDropdown"; + +const DROPDOWN_OPTIONS = [ + { disabled: false, label: "Edit", value: "edit-query" }, + { disabled: false, label: "Show query", value: "show-query" }, + { disabled: true, label: "Delete", value: "delete-query" }, +]; +const PLACEHOLDER = "Actions"; +const ON_CHANGE = (value: string) => { + console.log(value); +}; + +describe("Actions dropdown", () => { + it("renders dropdown placeholder and options", async () => { + const { user } = renderWithSetup( + + ); + + await user.click(screen.getByText("Actions")); + + expect(screen.queryAllByText(/edit/i)[1]).toBeInTheDocument(); // Aria shows Edit twice since it's focused + expect(screen.queryByText(/show query/i)).toBeInTheDocument(); + expect(screen.queryByText(/delete/i)).toBeInTheDocument(); + }); + + it("renders dropdown as disabled when disabled prop is true", () => { + renderWithSetup( + + ); + expect(screen.getByRole("combobox")).toBeDisabled(); + }); + + it("calls onChange with correct value when an option is selected", async () => { + const mockOnChange = jest.fn(); + const { user } = renderWithSetup( + + ); + + await user.click(screen.getByText("Actions")); + await user.click(screen.getByText("Edit")); + + expect(mockOnChange).toHaveBeenCalledWith("edit-query"); + }); + + it("renders disabled option as non-selectable", async () => { + const { user } = renderWithSetup( + + ); + + await user.click(screen.getByText("Actions")); + const deleteOption = screen.getByText("Delete"); + + expect(deleteOption).toHaveAttribute("aria-disabled", "true"); + }); + + it("closes the dropdown when clicking outside", async () => { + const { user } = renderWithSetup( + + ); + + await user.click(screen.getByText("Actions")); + expect(screen.getByText("Edit")).toBeVisible(); + + await user.click(document.body); + expect(screen.queryByText(/edit/i)).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/components/ActionsDropdown/ActionsDropdown.tsx b/frontend/components/ActionsDropdown/ActionsDropdown.tsx new file mode 100644 index 000000000000..9e068bf735d6 --- /dev/null +++ b/frontend/components/ActionsDropdown/ActionsDropdown.tsx @@ -0,0 +1,252 @@ +import React from "react"; +import Select, { + StylesConfig, + DropdownIndicatorProps, + OptionProps, + components, +} from "react-select-5"; + +import { PADDING } from "styles/var/padding"; +import { COLORS } from "styles/var/colors"; +import classnames from "classnames"; + +import { IDropdownOption } from "interfaces/dropdownOption"; + +import Icon from "components/Icon"; +import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper"; + +const baseClass = "actions-dropdown"; + +interface IActionsDropdownProps { + options: IDropdownOption[]; + placeholder: string; + onChange: (value: string) => void; + disabled?: boolean; + isSearchable?: boolean; + className?: string; + menuAlign?: "right" | "left" | "default"; +} + +const getOptionBackgroundColor = (state: any) => { + return state.isSelected || state.isFocused + ? COLORS["ui-vibrant-blue-10"] + : "transparent"; +}; + +const getLeftMenuAlign = (menuAlign: "right" | "left" | "default") => { + switch (menuAlign) { + case "right": + return "auto"; + case "left": + return "0"; + default: + return "-12px"; + } +}; + +const getRightMenuAlign = (menuAlign: "right" | "left" | "default") => { + switch (menuAlign) { + case "right": + return "0"; + default: + return "undefined"; + } +}; + +const CustomDropdownIndicator = ( + props: DropdownIndicatorProps +) => { + const { isFocused, selectProps } = props; + // no access to hover state here from react-select so that is done in the scss + // file of ActionsDropdown. + const color = + isFocused || selectProps.menuIsOpen + ? "core-fleet-blue" + : "core-fleet-black"; + + return ( + + + + ); +}; + +const CustomOption: React.FC> = (props) => { + const { innerProps, innerRef, data, isDisabled } = props; + + const optionContent = ( +
+ {data.label} + {data.helpText && ( + {data.helpText} + )} +
+ ); + + return ( + + {data.tooltipContent ? ( + + {optionContent} + + ) : ( + optionContent + )} + + ); +}; + +const ActionsDropdown = ({ + options, + placeholder, + onChange, + disabled, + isSearchable = false, + className, + menuAlign = "default", +}: IActionsDropdownProps): JSX.Element => { + const dropdownClassnames = classnames(baseClass, className); + + const handleChange = (newValue: IDropdownOption | null) => { + if (newValue) { + onChange(newValue.value.toString()); + } + }; + + const customStyles: StylesConfig = { + container: (provided) => ({ + ...provided, + width: "80px", + }), + control: (provided, state) => ({ + ...provided, + display: "flex", + flexDirection: "row", + width: "max-content", + padding: "8px 0", + backgroundColor: "initial", + border: 0, + boxShadow: "none", + cursor: "pointer", + "&:hover": { + boxShadow: "none", + ".actions-dropdown-select__placeholder": { + color: COLORS["core-vibrant-blue-over"], + }, + ".actions-dropdown-select__indicator path": { + stroke: COLORS["core-vibrant-blue-over"], + }, + }, + "&:active .actions-dropdown-select__indicator path": { + stroke: COLORS["core-vibrant-blue-down"], + }, + // TODO: Figure out a way to apply separate &:focus-visible styling + // Currently only relying on &:focus styling for tabbing through app + ...(state.menuIsOpen && { + ".actions-dropdown-select__indicator svg": { + transform: "rotate(180deg)", + transition: "transform 0.25s ease", + }, + }), + }), + placeholder: (provided, state) => ({ + ...provided, + color: state.isFocused + ? COLORS["core-fleet-blue"] + : COLORS["core-fleet-black"], + fontSize: "14px", + lineHeight: "normal", + paddingLeft: 0, + marginTop: "1px", + }), + dropdownIndicator: (provided) => ({ + ...provided, + display: "flex", + padding: "2px", + svg: { + transition: "transform 0.25s ease", + }, + }), + menu: (provided) => ({ + ...provided, + boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)", + borderRadius: "4px", + zIndex: 6, + overflow: "hidden", + border: 0, + marginTop: 0, + minWidth: "158px", + maxHeight: "220px", + position: "absolute", + left: getLeftMenuAlign(menuAlign), + right: getRightMenuAlign(menuAlign), + animation: "fade-in 150ms ease-out", + }), + menuList: (provided) => ({ + ...provided, + padding: PADDING["pad-small"], + }), + valueContainer: (provided) => ({ + ...provided, + padding: 0, + }), + option: (provided, state) => ({ + ...provided, + padding: "10px 8px", + fontSize: "14px", + backgroundColor: getOptionBackgroundColor(state), + "&:hover": { + backgroundColor: state.isDisabled + ? "transparent" + : COLORS["ui-vibrant-blue-10"], + }, + "&:active": { + backgroundColor: state.isDisabled + ? "transparent" + : COLORS["ui-vibrant-blue-10"], + }, + ...(state.isDisabled && { + color: COLORS["ui-fleet-black-50"], + fontStyle: "italic", + // pointerEvents: "none", // Prevents any mouse interaction + }), + }), + }; + + return ( +
+ + options={options} + placeholder={placeholder} + onChange={handleChange} + isDisabled={disabled} + isSearchable={isSearchable} + styles={customStyles} + components={{ + DropdownIndicator: CustomDropdownIndicator, + IndicatorSeparator: () => null, + Option: CustomOption, + SingleValue: () => null, // Doesn't replace placeholder text with selected text + // Note: react-select doesn't support skipping disabled options when keyboarding through + }} + controlShouldRenderValue={false} // Doesn't change placeholder text to selected text + isOptionSelected={() => false} // Hides any styling on selected option + className={dropdownClassnames} + classNamePrefix={`${baseClass}-select`} + isOptionDisabled={(option) => !!option.disabled} + /> +
+ ); +}; + +export default ActionsDropdown; diff --git a/frontend/components/ActionsDropdown/_styles.scss b/frontend/components/ActionsDropdown/_styles.scss new file mode 100644 index 000000000000..ba8549808dd8 --- /dev/null +++ b/frontend/components/ActionsDropdown/_styles.scss @@ -0,0 +1,6 @@ +// All styling in customStyles part of react-select-5 +.actions-dropdown-select__control { + &:focus-visible { + background-color: $core-fleet-blue; + } +} diff --git a/frontend/components/ActionsDropdown/index.ts b/frontend/components/ActionsDropdown/index.ts new file mode 100644 index 000000000000..92d81527a2c7 --- /dev/null +++ b/frontend/components/ActionsDropdown/index.ts @@ -0,0 +1 @@ +export { default } from "./ActionsDropdown"; diff --git a/frontend/components/Editor/Editor.tsx b/frontend/components/Editor/Editor.tsx index b724a296c39e..0ad54be1cf68 100644 --- a/frontend/components/Editor/Editor.tsx +++ b/frontend/components/Editor/Editor.tsx @@ -2,6 +2,7 @@ import classnames from "classnames"; import TooltipWrapper from "components/TooltipWrapper"; import React, { ReactNode } from "react"; import AceEditor from "react-ace"; +import { IAceEditor } from "react-ace/lib/types"; const baseClass = "editor"; @@ -66,6 +67,19 @@ const Editor = ({ [`${baseClass}__error`]: !!error, }); + const onLoadHandler = (editor: IAceEditor) => { + // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app + editor.commands.addCommand({ + name: "escapeToBlur", + bindKey: { win: "Esc", mac: "Esc" }, + exec: (aceEditor) => { + aceEditor.blur(); // Lose focus from the editor + return true; + }, + readOnly: true, + }); + }; + const renderLabel = () => { const labelText = error || label; const labelClassName = classnames(`${baseClass}__label`, { @@ -117,6 +131,7 @@ const Editor = ({ tabSize={2} focus={focus} onChange={onChange} + onLoad={onLoadHandler} /> {renderHelpText()}
diff --git a/frontend/components/FileUploader/FileUploader.tsx b/frontend/components/FileUploader/FileUploader.tsx index 6ba3bfa42f02..9951ae3509a0 100644 --- a/frontend/components/FileUploader/FileUploader.tsx +++ b/frontend/components/FileUploader/FileUploader.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useRef } from "react"; import classnames from "classnames"; import Button from "components/buttons/Button"; @@ -74,18 +74,32 @@ export const FileUploader = ({ fileDetails, }: IFileUploaderProps) => { const [isFileSelected, setIsFileSelected] = useState(!!fileDetails); + const fileInputRef = useRef(null); const classes = classnames(baseClass, className, { [`${baseClass}__file-preview`]: isFileSelected, }); const buttonVariant = buttonType === "button" ? "brand" : "text-icon"; + const triggerFileInput = () => { + fileInputRef.current?.click(); + }; + const onFileSelect = (e: React.ChangeEvent) => { const files = e.target.files; onFileUpload(files); setIsFileSelected(true); - e.target.value = ""; + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + triggerFileInput(); + } }; const renderGraphics = () => { @@ -113,6 +127,9 @@ export const FileUploader = ({ variant={buttonVariant} isLoading={isLoading} disabled={disabled} + customOnKeyDown={handleKeyDown} + tabIndex={0} + onClick={triggerFileInput} > { fixHotkeys(editor); + + // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app + editor.commands.addCommand({ + name: "escapeToBlur", + bindKey: { win: "Esc", mac: "Esc" }, + exec: (aceEditor) => { + aceEditor.blur(); // Lose focus from the editor + return true; + }, + readOnly: true, + }); + onLoad && onLoad(editor); }; diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index deb2d10c724b..8342002ac323 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -113,7 +113,7 @@ const Modal = ({ {title} {!disableClosingModal && (
-
diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx b/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx deleted file mode 100644 index 8c0c98852516..000000000000 --- a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import { screen } from "@testing-library/react"; -import { renderWithSetup } from "test/test-utils"; - -import DropdownCell from "./DropdownCell"; - -const DROPDOWN_OPTIONS = [ - { disabled: false, label: "Edit", value: "edit-query" }, - { disabled: false, label: "Show query", value: "show-query" }, - { disabled: true, label: "Delete", value: "delete-query" }, -]; -const PLACEHOLDER = "Actions"; -const ON_CHANGE = (value: string) => { - console.log(value); -}; - -describe("Dropdown cell", () => { - it("renders dropdown placeholder and options", async () => { - const { user } = renderWithSetup( - - ); - - await user.click(screen.getByText("Actions")); - - expect(screen.getByText(/edit/i)).toBeInTheDocument(); - expect(screen.getByText(/show query/i)).toBeInTheDocument(); - expect(screen.getByText(/delete/i)).toBeInTheDocument(); - }); -}); diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx b/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx deleted file mode 100644 index 6eb4fad3eab7..000000000000 --- a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; - -// ignore TS error for now until these are rewritten in ts. -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; - -import { IDropdownOption } from "interfaces/dropdownOption"; - -const baseClass = "dropdown-cell"; - -interface IDropdownCellProps { - options: IDropdownOption[]; - placeholder: string; - onChange: (value: string) => void; - disabled?: boolean; -} - -const DropdownCell = ({ - options, - placeholder, - onChange, - disabled, -}: IDropdownCellProps): JSX.Element => { - return ( -
- -
- ); -}; - -export default DropdownCell; diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss b/frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss deleted file mode 100644 index eaf1d1c3c254..000000000000 --- a/frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss +++ /dev/null @@ -1,87 +0,0 @@ -.dropdown-cell { - width: 80px; - - .form-field { - margin: 0; - } - - .Select { - position: relative; - border: 0; - height: auto; - - &.is-focused, - &:hover { - border: 0; - } - - &.is-focused:not(.is-open) { - .Select-control { - background-color: initial; - } - } - - &.is-disabled { - .Select-control { - .Select-placeholder { - @include disabled; - } - } - } - - .Select-control { - display: flex; - background-color: initial; - height: auto; - justify-content: space-between; - border: 0; - cursor: pointer; - - &:hover { - box-shadow: none; - } - - &:hover .Select-placeholder { - color: $core-vibrant-blue; - } - - .Select-placeholder { - color: $core-fleet-black; - font-size: 14px; - line-height: normal; - padding-left: 0; - margin-top: 1px; - } - - .Select-input { - height: auto; - } - - .Select-arrow-zone { - display: flex; - } - } - - .Select-menu-outer { - margin-top: $pad-xsmall; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); - border-radius: $border-radius; - z-index: 6; - overflow: hidden; - border: 0; - width: 188px; - left: unset; - top: unset; - max-height: 220px; - padding: $pad-small; - position: absolute; - left: -12px; - } - - &.is-open { - .Select-control .Select-placeholder { - color: $core-vibrant-blue; - } - } - } -} diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/index.ts b/frontend/components/TableContainer/DataTable/DropdownCell/index.ts deleted file mode 100644 index d2a9324aa2cf..000000000000 --- a/frontend/components/TableContainer/DataTable/DropdownCell/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./DropdownCell"; diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx index 31f2400d3dac..f96a0a77d29f 100644 --- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx +++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx @@ -85,6 +85,7 @@ const TeamsDropdown = ({ onChange={onChange} onOpen={onOpen} onClose={onClose} + tabIndex={0} /> ); } diff --git a/frontend/components/ViewAllHostsLink/_styles.scss b/frontend/components/ViewAllHostsLink/_styles.scss index 486834a65cec..1afb8b8f2282 100644 --- a/frontend/components/ViewAllHostsLink/_styles.scss +++ b/frontend/components/ViewAllHostsLink/_styles.scss @@ -7,4 +7,9 @@ } } } + + // For tabbing through the app + &:focus-visible.row-hover-link { + opacity: 1; + } } diff --git a/frontend/components/YamlAce/YamlAce.jsx b/frontend/components/YamlAce/YamlAce.jsx index 9632e68c2b75..b8979f99e8fa 100644 --- a/frontend/components/YamlAce/YamlAce.jsx +++ b/frontend/components/YamlAce/YamlAce.jsx @@ -17,6 +17,19 @@ class YamlAce extends Component { wrapperClassName: PropTypes.string, }; + onLoadHandler = (editor) => { + // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app + editor.commands.addCommand({ + name: "escapeToBlur", + bindKey: { win: "Esc", mac: "Esc" }, + exec: (aceEditor) => { + aceEditor.blur(); // Lose focus from the editor + return true; + }, + readOnly: true, + }); + }; + renderLabel = () => { const { name, error, label } = this.props; @@ -45,7 +58,7 @@ class YamlAce extends Component { wrapperClassName, } = this.props; - const { renderLabel } = this; + const { renderLabel, onLoadHandler } = this; const wrapperClass = classnames(wrapperClassName, "form-field", { [`${baseClass}__wrapper--error`]: error, @@ -67,6 +80,7 @@ class YamlAce extends Component { onChange={onChange} name={name} label={label} + onLoad={onLoadHandler} /> ); diff --git a/frontend/components/buttons/Button/Button.tsx b/frontend/components/buttons/Button/Button.tsx index 44b28fc4dbf5..48ee81ad00c9 100644 --- a/frontend/components/buttons/Button/Button.tsx +++ b/frontend/components/buttons/Button/Button.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { Children } from "react"; import classnames from "classnames"; import Spinner from "components/Spinner"; @@ -35,6 +35,7 @@ export interface IButtonProps { tabIndex?: number; type?: "button" | "submit" | "reset"; title?: string; + /** Default: "brand" */ variant?: ButtonVariant; onClick?: | ((value?: any) => void) @@ -44,6 +45,7 @@ export interface IButtonProps { | React.KeyboardEvent ) => void); isLoading?: boolean; + customOnKeyDown?: (e: React.KeyboardEvent) => void; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -57,7 +59,7 @@ class Button extends React.Component { static defaultProps = { size: "", type: "button", - variant: "default", + variant: "brand", }; componentDidMount(): void { @@ -115,6 +117,7 @@ class Button extends React.Component { title, variant, isLoading, + customOnKeyDown, } = this.props; const fullClassName = classnames( baseClass, @@ -136,7 +139,7 @@ class Button extends React.Component { className={fullClassName} disabled={disabled} onClick={handleClick} - onKeyDown={handleKeyDown} + onKeyDown={customOnKeyDown || handleKeyDown} tabIndex={tabIndex} type={type} title={title} diff --git a/frontend/components/buttons/Button/_styles.scss b/frontend/components/buttons/Button/_styles.scss index f38badbccec3..ca6fc095987e 100644 --- a/frontend/components/buttons/Button/_styles.scss +++ b/frontend/components/buttons/Button/_styles.scss @@ -301,8 +301,13 @@ $base-class: "button"; color: $core-vibrant-blue-down; } - &:focus { + &:focus-visible { + color: $core-vibrant-blue-over; + border: 1px; + border-radius: 2px; // Visble when tabbing + background: var(--Core-White, #fff); outline: none; + box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #d9d9fe; } &:hover, diff --git a/frontend/components/buttons/DropdownButton/_styles.scss b/frontend/components/buttons/DropdownButton/_styles.scss index 18f6d59871af..d22bb4e2089d 100644 --- a/frontend/components/buttons/DropdownButton/_styles.scss +++ b/frontend/components/buttons/DropdownButton/_styles.scss @@ -1,5 +1,5 @@ .dropdown-button { - padding: 8px 24px 8px 0; + padding: 8px 0; &__wrapper { display: flex; position: relative; diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss index c61763161579..7fcd24ad33ed 100644 --- a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss @@ -1,4 +1,7 @@ -.Select > .Select-menu-outer { +// Used with old react-select dropdown and +// New react-select-5 ActionsDropdown.tsx +.Select > .Select-menu-outer, +.actions-dropdown { .is-disabled * { color: $ui-fleet-black-50; } diff --git a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx index b93aea08dfda..089302eedc0f 100644 --- a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx +++ b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx @@ -12,7 +12,7 @@ import { IScheduledQuery } from "interfaces/scheduled_query"; import { IDropdownOption } from "interfaces/dropdownOption"; import Checkbox from "components/forms/fields/Checkbox"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; +import ActionsDropdown from "components/ActionsDropdown"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; @@ -54,7 +54,7 @@ interface IPerformanceImpactCellProps extends IRowProps { }; } -interface IDropdownCellProps extends IRowProps { +interface IActionsDropdownProps extends IRowProps { cell: { value: IDropdownOption[]; }; @@ -68,7 +68,7 @@ interface IDataColumn { Cell: | ((props: ICellProps) => JSX.Element) | ((props: IPerformanceImpactCellProps) => JSX.Element) - | ((props: IDropdownCellProps) => JSX.Element); + | ((props: IActionsDropdownProps) => JSX.Element); disableHidden?: boolean; disableSortBy?: boolean; } @@ -182,8 +182,8 @@ const generateTableHeaders = ( Header: "", disableSortBy: true, accessor: "actions", - Cell: (cellProps: IDropdownCellProps) => ( - ( + actionSelectHandler(value, cellProps.row.original) diff --git a/frontend/components/top_nav/UserMenu/UserMenu.tsx b/frontend/components/top_nav/UserMenu/UserMenu.tsx index 0cae736a3246..b1228abc25ce 100644 --- a/frontend/components/top_nav/UserMenu/UserMenu.tsx +++ b/frontend/components/top_nav/UserMenu/UserMenu.tsx @@ -71,7 +71,7 @@ const UserMenu = ({ return (
- + void; + router: InjectedRouter; } const DiskEncryption = ({ currentTeamId, onMutation, + router, }: IDiskEncryptionProps) => { const { isPremiumTier, config, setConfig } = useContext(AppContext); const { renderFlash } = useContext(NotificationContext); @@ -123,7 +126,10 @@ const DiskEncryption = ({ ) : (
{showAggregate && ( - + )} { +const DiskEncryptionTable = ({ + currentTeamId, + router, +}: IDiskEncryptionTableProps) => { const { data: diskEncryptionStatusData, error: diskEncryptionStatusError, @@ -31,6 +50,21 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => { } ); + const onSelectSingleRow = useCallback( + (row: IDiskEncryptionRowProps) => { + const { status, teamId } = row.original; + + const queryParams = { + [HOSTS_QUERY_PARAMS.DISK_ENCRYPTION]: status?.value, + team_id: teamId, + }; + const endpoint = PATHS.MANAGE_HOSTS; + const path = `${endpoint}?${buildQueryStringFromParams(queryParams)}`; + router.push(path); + }, + [router] + ); + const tableHeaders = generateTableHeaders(); const tableData = generateTableData(diskEncryptionStatusData, currentTeamId); @@ -60,6 +94,9 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => { catches up." /> )} + // these 2 properties allow linking on click anywhere in the row + disableMultiRowSelect + onSelectSingleRow={onSelectSingleRow} />
); diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx index a5522d011cc7..1eddba30ad07 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx @@ -5,7 +5,6 @@ import { IDiskEncryptionStatusAggregate, IDiskEncryptionSummaryResponse, } from "services/entities/mdm"; -import { HOSTS_QUERY_PARAMS } from "services/entities/hosts"; import TextCell from "components/TableContainer/DataTable/TextCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -13,7 +12,7 @@ import StatusIndicatorWithIcon from "components/StatusIndicatorWithIcon"; import ViewAllHostsLink from "components/ViewAllHostsLink"; import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon"; -interface IStatusCellValue { +export interface IStatusCellValue { displayName: string; statusName: IndicatorStatus; value: DiskEncryptionStatus; @@ -118,15 +117,7 @@ const defaultTableHeaders: IDataColumn[] = [ return ( <> {cellProps.row.original && ( - + )} ); diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx index 0a8863abe646..152522c55961 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx @@ -16,10 +16,9 @@ import { buildQueryStringFromParams } from "utilities/url"; import { internationalTimeFormat } from "utilities/helpers"; import { uploadedFromNow } from "utilities/date_format"; -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; import Card from "components/Card"; import Graphic from "components/Graphic"; +import ActionsDropdown from "components/ActionsDropdown"; import TooltipWrapper from "components/TooltipWrapper"; import DataSet from "components/DataSet"; import Icon from "components/Icon"; @@ -183,7 +182,7 @@ interface IActionsDropdownProps { onEditSoftwareClick: () => void; } -const ActionsDropdown = ({ +const SoftwareActionsDropdown = ({ isSoftwarePackage, onDownloadClick, onDeleteClick, @@ -207,16 +206,17 @@ const ActionsDropdown = ({ return (
-
); @@ -353,7 +353,7 @@ const SoftwarePackageCard = ({
)} {showActions && ( - JSX.Element) - | ((props: IDropdownCellProps) => JSX.Element); + | ((props: IActionsDropdownProps) => JSX.Element); disableHidden?: boolean; disableSortBy?: boolean; sortType?: string; @@ -98,8 +98,8 @@ const generateTableHeaders = ( Header: "", disableSortBy: true, accessor: "actions", - Cell: (cellProps: IDropdownCellProps) => ( - ( + actionSelectHandler(value, cellProps.row.original) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx index d79e1d3eb16c..03a633b599bb 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx @@ -6,7 +6,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import { IDropdownOption } from "interfaces/dropdownOption"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; +import ActionsDropdown from "components/ActionsDropdown"; import TextCell from "components/TableContainer/DataTable/TextCell"; import TooltipWrapper from "components/TooltipWrapper"; @@ -163,7 +163,7 @@ export const generateTableConfig = ( // but we don't use it. accessor: "id", Cell: (cellProps) => ( - actionSelectHandler(value, cellProps.row.original) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx index 28e1e32cae02..2a10c65bf77e 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx @@ -6,7 +6,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import { IDropdownOption } from "interfaces/dropdownOption"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; +import ActionsDropdown from "components/ActionsDropdown"; import TextCell from "components/TableContainer/DataTable/TextCell"; import RenewDateCell from "../../../components/RenewDateCell"; @@ -104,7 +104,7 @@ export const generateTableConfig = ( // but we don't use it. accessor: "id", Cell: (cellProps) => ( - actionSelectHandler(value, cellProps.row.original) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx index a3f50e6e2f79..6bc821b46846 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx @@ -114,6 +114,7 @@ const IdpSection = () => { disabled={!completedForm} onClick={onSubmit} className="button-wrap" + variant="brand" > Save diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 49c66aaab7b3..6a564141de15 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -295,12 +295,11 @@ const TeamDetailsWrapper = ({ try { await teamsAPI.destroy(teamIdForApi); - return router.push(PATHS.ADMIN_TEAMS); + router.push(PATHS.ADMIN_TEAMS); renderFlash("success", "Team removed"); } catch (response) { renderFlash("error", "Something went wrong removing the team"); console.error(response); - return false; } finally { toggleDeleteTeamModal(); setIsUpdatingTeams(false); diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx index 20d166e251ee..6cb769b90df6 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx @@ -1,7 +1,7 @@ import React from "react"; import ReactTooltip from "react-tooltip"; import TextCell from "components/TableContainer/DataTable/TextCell/TextCell"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; +import ActionsDropdown from "components/ActionsDropdown"; import CustomLink from "components/CustomLink"; import { IUser, UserRole } from "interfaces/user"; import { ITeam } from "interfaces/team"; @@ -29,7 +29,7 @@ interface ICellProps extends IRowProps { }; } -interface IDropdownCellProps extends IRowProps { +interface IActionsDropdownProps extends IRowProps { cell: { value: IDropdownOption[]; }; @@ -41,7 +41,7 @@ interface IDataColumn { accessor: string; Cell: | ((props: ICellProps) => JSX.Element) - | ((props: IDropdownCellProps) => JSX.Element); + | ((props: IActionsDropdownProps) => JSX.Element); disableHidden?: boolean; disableSortBy?: boolean; sortType?: string; @@ -174,8 +174,8 @@ const generateColumnConfigs = ( Header: "", disableSortBy: true, accessor: "actions", - Cell: (cellProps: IDropdownCellProps) => ( - ( + actionSelectHandler(value, cellProps.row.original) diff --git a/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx b/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx index 55edae36d2cc..31a7de9ebee2 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx @@ -2,7 +2,7 @@ import React from "react"; import LinkCell from "components/TableContainer/DataTable/LinkCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; +import DropdownCell from "components/ActionsDropdown"; import { ITeam } from "interfaces/team"; import { IDropdownOption } from "interfaces/dropdownOption"; import PATHS from "router/paths"; diff --git a/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx b/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx index 825eda6aea88..bd013cfba084 100644 --- a/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx +++ b/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx @@ -20,7 +20,7 @@ const DeleteTeamModal = ({ }: IDeleteTeamModalProps): JSX.Element => { return ( JSX.Element) - | ((props: IDropdownCellProps) => JSX.Element); + | ((props: IActionsDropdownProps) => JSX.Element); disableHidden?: boolean; disableSortBy?: boolean; } @@ -200,8 +200,8 @@ const generateTableHeaders = ( Header: "", disableSortBy: true, accessor: "actions", - Cell: (cellProps: IDropdownCellProps) => ( - ( + actionSelectHandler(value, cellProps.row.original) diff --git a/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx b/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx index f46dfaf4ae1f..2a4885499c09 100644 --- a/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx +++ b/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx @@ -54,7 +54,7 @@ const HostStatusWebhookPreviewModal = ({ />
-
diff --git a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx index f6e564b21688..729bd31bc61e 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx @@ -30,7 +30,7 @@ const CustomLabelGroupHeading = ( const handleInputClick = ( event: React.MouseEvent ) => { - onClickLabelSearchInput(event); + onClickLabelSearchInput && onClickLabelSearchInput(event); inputRef.current?.focus(); event.stopPropagation(); }; diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss index 8f4a9c76a377..a71973b8636a 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss @@ -17,11 +17,6 @@ padding: 0px; border: none; margin-left: 0; - - img { - padding: 0px; - margin: 0px; - } } .premium-icon-tip { diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx index 355e87d66550..40c0dcc9ede1 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx @@ -24,12 +24,12 @@ declare module "react-select-5/dist/declarations/src/Select" { IsMulti extends boolean, Group extends GroupBase