From e750eb97459dbc043953f02813ec1b083a023d75 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Wed, 1 Dec 2021 17:37:33 -0600 Subject: [PATCH] Standardize TeamsDropdown component usage (#3135) --- changes/fix-2996-schedule-dropdown | 1 + .../premium/team_maintainer_observer.spec.ts | 2 +- cypress/integration/premium/teamflow.spec.ts | 7 +- .../TeamsDropdown/TeamsDropdown.tsx | 99 ++++--- .../components/TeamsDropdown/_styles.scss | 7 +- .../side_panels/SiteTopNav/navItems.js | 4 +- frontend/fleet/helpers.ts | 31 +-- frontend/pages/Homepage/Homepage.tsx | 153 ++++++----- frontend/pages/Homepage/_styles.scss | 22 +- .../Homepage/cards/ActivityFeed/_styles.scss | 6 + .../pages/admin/SettingsWrapper/_styles.scss | 6 +- .../TeamDetailsWrapper/TeamDetailsWrapper.tsx | 15 +- .../TeamDetailsWrapper/_styles.scss | 5 +- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 36 ++- .../pages/hosts/ManageHostsPage/_styles.scss | 3 +- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 78 +++--- .../policies/ManagePoliciesPage/_styles.scss | 3 +- .../TeamsDropdown/TeamsDropdown.tsx | 69 ----- .../components/TeamsDropdown/_styles.scss | 84 ------ .../components/TeamsDropdown/index.ts | 1 - .../ManageQueriesPage/ManageQueriesPage.tsx | 8 +- .../queries/ManageQueriesPage/_styles.scss | 9 +- .../ManageSchedulePage/ManageSchedulePage.tsx | 248 ++++++------------ .../schedule/ManageSchedulePage/_styles.scss | 87 +----- frontend/styles/global/_global.scss | 3 +- 25 files changed, 355 insertions(+), 632 deletions(-) create mode 100644 changes/fix-2996-schedule-dropdown delete mode 100644 frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/TeamsDropdown.tsx delete mode 100644 frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/_styles.scss delete mode 100644 frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/index.ts diff --git a/changes/fix-2996-schedule-dropdown b/changes/fix-2996-schedule-dropdown new file mode 100644 index 0000000000..cbbb3a1ae6 --- /dev/null +++ b/changes/fix-2996-schedule-dropdown @@ -0,0 +1 @@ +* Fix manage schedule page teams dropdown cutting off in some cases \ No newline at end of file diff --git a/cypress/integration/premium/team_maintainer_observer.spec.ts b/cypress/integration/premium/team_maintainer_observer.spec.ts index e09a60d282..24ff95d7ac 100644 --- a/cypress/integration/premium/team_maintainer_observer.spec.ts +++ b/cypress/integration/premium/team_maintainer_observer.spec.ts @@ -143,7 +143,7 @@ describe( cy.findByRole("button", { name: /done/i }).click(); // See the "Manage" enroll secret” button on team Oranges only - cy.findByText(/all teams/i).should("exist"); + cy.findAllByText(/apples/i).should("exist"); cy.findByText(/manage enroll secret/i).should("not.exist"); cy.visit("/hosts/manage/?team_id=1"); diff --git a/cypress/integration/premium/teamflow.spec.ts b/cypress/integration/premium/teamflow.spec.ts index 87c88d0eb0..f28ca02411 100644 --- a/cypress/integration/premium/teamflow.spec.ts +++ b/cypress/integration/premium/teamflow.spec.ts @@ -102,12 +102,7 @@ describe("Teams flow", () => { cy.visit("/schedule/manage"); cy.wait(2000); // eslint-disable-line cypress/no-unnecessary-waiting - cy.findByText(/all teams/i).click(); - cy.findByText(/valor/i).click(); - - cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting - cy.findByText(/query all window crashes/i).should("not.exist"); - cy.findByText(/inherited query/i).click(); + cy.findByText(/valor/i).should("exist"); cy.findByText(/query all window crashes/i).should("exist"); // Edit Team diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx index 1cf981108d..3c9e872672 100644 --- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx +++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx @@ -1,20 +1,39 @@ -import React, { useContext } from "react"; +import React, { useContext, useMemo } from "react"; import { AppContext } from "context/app"; - import { ITeam } from "interfaces/team"; -import { - generateTeamFilterDropdownOptions, - getValidatedTeamId, -} from "fleet/helpers"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; +const generateDropdownOptions = ( + teams: ITeam[] | undefined, + includeAll: boolean | undefined +) => { + if (!teams) { + return []; + } + + const options = teams.map((team) => ({ + disabled: false, + label: team.name, + value: team.id, + })); + + if (includeAll) { + options.unshift({ + disabled: false, + label: "All teams", + value: 0, + }); + } + + return options; +}; + interface ITeamsDropdownProps { - isLoading: boolean; - teams: ITeam[]; - currentTeamId: number; - hideAllTeamsOption?: boolean; + currentUserTeams: ITeam[]; + selectedTeamId: number; + includeAll?: boolean; onChange: (newSelectedValue: number) => void; onOpen?: () => void; onClose?: () => void; @@ -23,47 +42,41 @@ interface ITeamsDropdownProps { const baseClass = "component__team-dropdown"; const TeamsDropdown = ({ - isLoading, - teams, - currentTeamId, - hideAllTeamsOption = false, + currentUserTeams, + selectedTeamId, + includeAll, onChange, onOpen, onClose, -}: ITeamsDropdownProps) => { - const { currentUser, isPremiumTier, isOnGlobalTeam } = useContext(AppContext); +}: ITeamsDropdownProps): JSX.Element => { + const { isOnGlobalTeam } = useContext(AppContext); - if (isLoading) { - return null; - } else if (!isPremiumTier) { - return

Hosts

; - } + const teamOptions = useMemo( + () => + generateDropdownOptions(currentUserTeams, isOnGlobalTeam || includeAll), + [currentUserTeams, isOnGlobalTeam] + ); - const teamOptions = generateTeamFilterDropdownOptions( - teams, - currentUser, - isOnGlobalTeam as boolean, - hideAllTeamsOption - ); - const selectedTeamId = getValidatedTeamId( - teams, - currentTeamId, - currentUser, - isOnGlobalTeam as boolean - ); + const selectedValue = teamOptions.find( + (option) => selectedTeamId === option.value + ) + ? selectedTeamId + : teamOptions[0]?.value; return (
- + {teamOptions.length && ( + + )}
); }; diff --git a/frontend/components/TeamsDropdown/_styles.scss b/frontend/components/TeamsDropdown/_styles.scss index f706ddd2b4..19b12cc6cc 100644 --- a/frontend/components/TeamsDropdown/_styles.scss +++ b/frontend/components/TeamsDropdown/_styles.scss @@ -38,10 +38,11 @@ } .Select-arrow-zone { + height: 32px; padding-left: $pad-small; .Select-arrow { - top: 1px !important; + top: 2px !important; margin-top: 0 !important; } } @@ -65,7 +66,7 @@ right: 0; left: 0; bottom: 0; - top: 0; + top: 2px; &.is-focused { border: 0 !important; @@ -80,4 +81,4 @@ } } } -} \ No newline at end of file +} diff --git a/frontend/components/side_panels/SiteTopNav/navItems.js b/frontend/components/side_panels/SiteTopNav/navItems.js index a26aede8fc..e2b97d3631 100644 --- a/frontend/components/side_panels/SiteTopNav/navItems.js +++ b/frontend/components/side_panels/SiteTopNav/navItems.js @@ -1,6 +1,7 @@ import PATHS from "router/paths"; import URL_PREFIX from "router/url_prefix"; import permissionUtils from "utilities/permissions"; +import { getSortedTeamOptions } from "fleet/helpers"; export default (currentUser) => { const logo = [ @@ -67,6 +68,7 @@ export default (currentUser) => { const userAdminTeams = currentUser.teams.filter( (thisTeam) => thisTeam.role === "admin" ); + const sortedTeams = getSortedTeamOptions(userAdminTeams); const adminNavItems = [ { icon: "settings", @@ -77,7 +79,7 @@ export default (currentUser) => { pathname: currentUser.global_role === "admin" ? PATHS.ADMIN_SETTINGS - : `${PATHS.ADMIN_TEAMS}/${userAdminTeams[0].id}/members`, + : `${PATHS.ADMIN_TEAMS}/${sortedTeams[0].value}/members`, }, }, ]; diff --git a/frontend/fleet/helpers.ts b/frontend/fleet/helpers.ts index 63274b2c23..4a0327c087 100644 --- a/frontend/fleet/helpers.ts +++ b/frontend/fleet/helpers.ts @@ -675,7 +675,7 @@ export const syntaxHighlight = (json: any): string => { /* eslint-enable no-useless-escape */ }; -const getSortedTeamOptions = memoize((teams: ITeam[]) => +export const getSortedTeamOptions = memoize((teams: ITeam[]) => teams .map((team) => { return { @@ -687,34 +687,6 @@ const getSortedTeamOptions = memoize((teams: ITeam[]) => .sort((a, b) => sortUtils.caseInsensitiveAsc(a.label, b.label)) ); -export const generateTeamFilterDropdownOptions = ( - teams: ITeam[], - currentUser: IUser | null, - isOnGlobalTeam: boolean, - hideAllTeamsOption: boolean -) => { - let currentUserTeams: ITeam[] = []; - if (isOnGlobalTeam) { - currentUserTeams = teams; - } else if (currentUser && currentUser.teams) { - currentUserTeams = currentUser.teams; - } - - const allTeamOption = [ - { - disabled: false, - label: "All teams", - value: 0, - }, - ]; - - const sortedCurrentUserTeamOptions = getSortedTeamOptions(currentUserTeams); - - return !hideAllTeamsOption - ? allTeamOption.concat(sortedCurrentUserTeamOptions) - : sortedCurrentUserTeamOptions; -}; - export const getValidatedTeamId = ( teams: ITeam[], teamId: number, @@ -766,6 +738,5 @@ export default { setupData, frontendFormattedConfig, syntaxHighlight, - generateTeamFilterDropdownOptions, getValidatedTeamId, }; diff --git a/frontend/pages/Homepage/Homepage.tsx b/frontend/pages/Homepage/Homepage.tsx index c339c65920..06f86a988d 100644 --- a/frontend/pages/Homepage/Homepage.tsx +++ b/frontend/pages/Homepage/Homepage.tsx @@ -1,18 +1,17 @@ import React, { useContext, useState } from "react"; import { useQuery } from "react-query"; import paths from "router/paths"; -import { Link } from "react-router"; import { AppContext } from "context/app"; import { find } from "lodash"; import hostSummaryAPI from "services/entities/host_summary"; import teamsAPI from "services/entities/teams"; import { IHostSummary, IHostSummaryPlatforms } from "interfaces/host_summary"; -import { ISoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; +import { getSortedTeamOptions } from "fleet/helpers"; +import sortUtils from "utilities/sort"; import TeamsDropdown from "components/TeamsDropdown"; -import Button from "components/buttons/Button"; import InfoCard from "./components/InfoCard"; import HostsStatus from "./cards/HostsStatus"; import HostsSummary from "./cards/HostsSummary"; @@ -20,7 +19,6 @@ import ActivityFeed from "./cards/ActivityFeed"; import Software from "./cards/Software"; import LearnFleet from "./cards/LearnFleet"; import WelcomeHost from "./cards/WelcomeHost"; -import LinkArrow from "../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png"; interface ITeamsResponse { teams: ITeam[]; @@ -41,6 +39,7 @@ const Homepage = (): JSX.Element => { currentTeam, isPremiumTier, isPreviewMode, + isOnGlobalTeam, setCurrentTeam, } = useContext(AppContext); @@ -60,7 +59,17 @@ const Homepage = (): JSX.Element => { ITeam[] >(["teams"], () => teamsAPI.loadAll(), { enabled: !!isPremiumTier, - select: (data: ITeamsResponse) => data.teams, + select: (data: ITeamsResponse) => + data.teams.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name)), + onSuccess: (responseTeams) => { + if (!isOnGlobalTeam) { + const sortedTeams = getSortedTeamOptions(responseTeams); + const firstTeamOption = responseTeams.find( + (responseTeam) => responseTeam.id === sortedTeams[0].value + ); + setCurrentTeam(firstTeamOption); + } + }, }); const handleTeamSelect = (teamId: number) => { @@ -94,68 +103,72 @@ const Homepage = (): JSX.Element => { return (
-
-
- {isPremiumTier ? ( - - handleTeamSelect(newSelectedValue) - } +
+
+
+
+
+ {isPremiumTier && teams && teams.length > 1 && ( + + handleTeamSelect(newSelectedValue) + } + /> + )} + {isPremiumTier && teams && teams.length === 1 && ( +

{teams[0].name}

+ )} + {!isPremiumTier &&

{config?.org_name}

} +
+
+
+
+
+ + - ) : ( -

- {config?.org_name} -

- )} +
-
-
- + + + +
+ {isPreviewMode && ( +
+ + + + + + +
+ )} +
- - -
-
- - - -
- {isPreviewMode && ( -
- - - - - - -
- )} -
- {!currentTeam && ( { setIsSoftwareModalOpen={setIsSoftwareModalOpen} /> - )} - {!isPreviewMode && !currentTeam && ( - - - - )} + {!isPreviewMode && !currentTeam && isOnGlobalTeam && ( + + + + )} +
); diff --git a/frontend/pages/Homepage/_styles.scss b/frontend/pages/Homepage/_styles.scss index e4b2566988..1ba4ff0859 100644 --- a/frontend/pages/Homepage/_styles.scss +++ b/frontend/pages/Homepage/_styles.scss @@ -1,18 +1,18 @@ .homepage { - min-height: 0; - padding: $pad-xxlarge; - width: 100%; - flex: 1; background-color: $ui-off-white; - box-sizing: border-box; + flex: 1; + .homepage__wrapper { + background-color: $ui-off-white; + } h2 { font-size: $small; font-weight: $regular; margin: 0; } - + &__header-wrap { + height: 38px; display: flex; align-items: center; justify-content: space-between; @@ -22,15 +22,17 @@ &__header { display: flex; align-items: center; - + + .form-field--dropdown { + margin-bottom: 0; + } + .Select-control { background-color: $ui-off-white !important; } } &__title { - font-size: $large; - .fleeticon { color: $core-fleet-blue; margin-right: 15px; @@ -57,7 +59,7 @@ &.two-column { row-gap: $pad-medium; } - + @media screen and (min-width: 990px) { &.two-column { grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss b/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss index 042e00b261..3ea3209d06 100644 --- a/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss +++ b/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss @@ -3,6 +3,12 @@ display: flex; flex-direction: column; + &__header-wrap { + .form-field { + margin-bottom: 0; + } + } + &__block { display: flex; flex-direction: row; diff --git a/frontend/pages/admin/SettingsWrapper/_styles.scss b/frontend/pages/admin/SettingsWrapper/_styles.scss index 507881a745..2211fdc968 100644 --- a/frontend/pages/admin/SettingsWrapper/_styles.scss +++ b/frontend/pages/admin/SettingsWrapper/_styles.scss @@ -2,12 +2,14 @@ .settings-wrapper { h1 { - margin-bottom: $pad-small; + display: flex; + align-items: center; + height: 38px; position: relative; // fake padding for h1 while sticky &::before { - content: ''; + content: ""; width: 100%; height: $pad-xxlarge; position: absolute; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 6847a4a69c..d61a73d43b 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -103,7 +103,9 @@ const TeamDetailsWrapper = ({ location: { pathname }, params: routeParams, }: ITeamDetailsPageProps): JSX.Element => { - const { isGlobalAdmin, setCurrentTeam } = useContext(AppContext); + const { isGlobalAdmin, isOnGlobalTeam, setCurrentTeam } = useContext( + AppContext + ); const isLoadingTeams = useSelector( (state: IRootState) => state.entities.teams.loading @@ -213,8 +215,9 @@ const TeamDetailsWrapper = ({ setTeamMenuIsOpen(false); }; - const teamDetailsClasses = classnames(baseClass, { + const teamWrapperClasses = classnames(baseClass, { "team-select-open": teamMenuIsOpen, + "team-settings": !isOnGlobalTeam, }); if (isLoadingTeams || team === undefined) { @@ -232,7 +235,7 @@ const TeamDetailsWrapper = ({ const adminTeams = isGlobalAdmin ? teams : userAdminTeams; return ( -
+
<> {isGlobalAdmin && ( @@ -248,10 +251,8 @@ const TeamDetailsWrapper = ({

{team.name}

) : ( handleTeamSelect(newSelectedValue) } diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss index dafc2aea8f..7f70ec37a2 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/_styles.scss @@ -1,6 +1,10 @@ .team-details { padding: 25px $pad-xlarge 50px; // different to pad sticky subnav properly + &.team-settings { + padding: $pad-xxlarge; + } + &__loading-spinner { // NOTE: this value was chosen just cause it looked right. Might want to // come back and change it later to be more exact. @@ -23,7 +27,6 @@ } &__team-header { - margin-top: $pad-large; display: flex; justify-content: space-between; margin-bottom: $pad-medium; diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index a9b63bae8e..bf80d0ae0d 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -35,6 +35,7 @@ import { IStatusLabels } from "interfaces/status_labels"; import { ITeam } from "interfaces/team"; import { useDeepEffect } from "utilities/hooks"; // @ts-ignore import deepDifference from "utilities/deep_difference"; +import sortUtils from "utilities/sort"; import { PLATFORM_LABEL_DISPLAY_NAMES, PolicyResponse, @@ -312,7 +313,13 @@ const ManageHostsPage = ({ () => teamsAPI.loadAll(), { enabled: !!isPremiumTier, - select: (data: ITeamsResponse) => data.teams, + select: (data: ITeamsResponse) => + data.teams.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name)), + onSuccess: (responseTeams: ITeam[]) => { + if (responseTeams.length === 1) { + setCurrentTeam(responseTeams[0]); + } + }, } ); @@ -1066,9 +1073,8 @@ const ManageHostsPage = ({ const renderTeamsFilterDropdown = () => ( @@ -1333,7 +1339,16 @@ const ManageHostsPage = ({ return (
- {renderTeamsFilterDropdown()} +
+ {!isPremiumTier &&

Hosts

} + {isPremiumTier && + teams && + teams.length > 1 && + renderTeamsFilterDropdown()} + {isPremiumTier && teams && teams.length === 1 && ( +

{teams[0].name}

+ )} +
); @@ -1597,10 +1612,13 @@ const ManageHostsPage = ({
{renderActiveFilterBlock()} -
- {renderNoEnrollSecretBanner()} - {renderSoftwareVulnerabilities()} -
+ {renderNoEnrollSecretBanner() || + (renderSoftwareVulnerabilities() && ( +
+ {renderNoEnrollSecretBanner()} + {renderSoftwareVulnerabilities()} +
+ ))} {config && (!isPremiumTier || teams) && renderTable(selectedTeam)}
)} diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index ef85e92a01..f87a6a7ac2 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -1,9 +1,10 @@ .manage-hosts { .header-wrap { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; margin-bottom: $pad-medium; + height: 38px; .button-wrap { display: flex; diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index b13bf89721..24c06fe1c4 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -29,9 +29,9 @@ import TableDataError from "components/TableDataError"; import Button from "components/buttons/Button"; import InfoBanner from "components/InfoBanner/InfoBanner"; import IconToolTip from "components/IconToolTip"; +import TeamsDropdown from "components/TeamsDropdown"; import PoliciesListWrapper from "./components/PoliciesListWrapper"; import RemovePoliciesModal from "./components/RemovePoliciesModal"; -import TeamsDropdown from "./components/TeamsDropdown"; const baseClass = "manage-policies-page"; @@ -77,16 +77,16 @@ const ManagePolicyPage = (managePoliciesPageProps: { isTeamMaintainer(user, teamId) || isTeamAdmin(user, teamId); - const { data: teams } = useQuery<{ teams: ITeam[] }, Error, ITeam[]>( - ["teams"], - () => teamsAPI.loadAll({}), - { - enabled: !!isPremiumTier, - select: (data) => data.teams, - refetchOnMount: false, - refetchOnWindowFocus: false, - } - ); + const { data: teams, isLoading: isLoadingTeams } = useQuery< + { teams: ITeam[] }, + Error, + ITeam[] + >(["teams"], () => teamsAPI.loadAll({}), { + enabled: !!isPremiumTier, + select: (data) => data.teams, + refetchOnMount: false, + refetchOnWindowFocus: false, + }); const { data: fleetQueries } = useQuery( ["fleetQueries"], @@ -110,8 +110,8 @@ const ManagePolicyPage = (managePoliciesPageProps: { const [isLoadingTeamPolicies, setIsLoadingTeamPolicies] = useState(true); const [isTeamPoliciesError, setIsTeamPoliciesError] = useState(false); const [userTeams, setUserTeams] = useState(null); - const [selectedTeamId, setSelectedTeamId] = useState( - parseInt(location?.query?.team_id, 10) || null + const [selectedTeamId, setSelectedTeamId] = useState( + parseInt(location?.query?.team_id, 10) || 0 ); const [selectedPolicyIds, setSelectedPolicyIds] = useState< number[] | never[] @@ -166,7 +166,7 @@ const ManagePolicyPage = (managePoliciesPageProps: { [getGlobalPolicies, getTeamPolicies] ); - const handleChangeSelectedTeam = (id: number) => { + const handleTeamSelect = (id: number) => { const { MANAGE_POLICIES } = PATHS; const path = id ? `${MANAGE_POLICIES}?team_id=${id}` : MANAGE_POLICIES; router.replace(path); @@ -249,7 +249,7 @@ const ManagePolicyPage = (managePoliciesPageProps: { if (isOnGlobalTeam) { // For global users, default to zero (i.e. all teams). if (teamId !== 0) { - handleChangeSelectedTeam(0); + handleTeamSelect(0); return; } } else { @@ -258,7 +258,7 @@ const ManagePolicyPage = (managePoliciesPageProps: { // API request will not be triggered. teamId = userTeams[0]?.id || null; if (teamId) { - handleChangeSelectedTeam(teamId); + handleTeamSelect(teamId); return; } } @@ -329,14 +329,20 @@ const ManagePolicyPage = (managePoliciesPageProps: {
{isFreeTier &&

Policies

} {isPremiumTier && - userTeams !== null && - selectedTeamId !== null && ( + userTeams && + userTeams.length > 1 && + selectedTeamId >= 0 && ( + handleTeamSelect(newSelectedValue) + } /> )} + {isPremiumTier && userTeams && userTeams.length === 1 && ( +

{userTeams[0].name}

+ )}
@@ -351,20 +357,22 @@ const ManagePolicyPage = (managePoliciesPageProps: { )} -
- {isPremiumTier && !!selectedTeamId && ( -

- Add additional policies for all hosts assigned to this team - . -

- )} - {showDefaultDescription && ( -

- Add policies for all of your hosts to see which pass your - organization’s standards.{" "} -

- )} -
+ {!isLoadingTeams && ( +
+ {isPremiumTier && !!selectedTeamId && ( +

+ Add additional policies for{" "} + all hosts assigned to this team. +

+ )} + {showDefaultDescription && ( +

+ Add policies for all of your hosts to see which pass your + organization’s standards.{" "} +

+ )} +
+ )} {!!updateInterval && showInfoBanner && (

diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index 280573d619..4c997acb4a 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -8,7 +8,6 @@ align-items: center; justify-content: space-between; height: 38px; - margin-bottom: $pad-small; } &__header { @@ -130,7 +129,7 @@ top: 0; border: 0; cursor: pointer; - + @include button-variant( $core-vibrant-blue, $core-vibrant-blue-over, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/TeamsDropdown.tsx deleted file mode 100644 index 746c8b608f..0000000000 --- a/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/TeamsDropdown.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useContext, useMemo } from "react"; -import { isEmpty } from "lodash"; -import { AppContext } from "context/app"; - -import { ITeam } from "interfaces/team"; - -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; - -const generateDropdownOptions = ( - teams: ITeam[] | undefined, - includeAll: boolean | undefined -) => { - if (!teams) { - return []; - } - - const options = teams.map((team) => ({ - disabled: false, - label: team.name, - value: team.id, - })); - - if (includeAll) { - options.unshift({ - disabled: false, - label: "All teams", - value: 0, - }); - } - - return options; -}; - -const TeamsDropdown = (dropdownProps: { - currentUserTeams: ITeam[]; - onChange: (id: number) => void; - selectedTeam: number; -}): JSX.Element => { - const { currentUserTeams, onChange, selectedTeam } = dropdownProps; - const { isOnGlobalTeam } = useContext(AppContext); - - const dropdownOptions = useMemo( - () => generateDropdownOptions(currentUserTeams, isOnGlobalTeam), - [currentUserTeams, isOnGlobalTeam] - ); - - const selectedValue = dropdownOptions.find( - (option) => selectedTeam === option.value - ) - ? selectedTeam - : dropdownOptions[0]?.value; - - return isEmpty(currentUserTeams) ? ( -

Policies

- ) : ( -
- -
- ); -}; -export default TeamsDropdown; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/_styles.scss deleted file mode 100644 index dacc0cbb33..0000000000 --- a/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/_styles.scss +++ /dev/null @@ -1,84 +0,0 @@ -.teams-dropdown { - border: 0 !important; - position: relative; - - :hover { - cursor: pointer !important; - } - - &.is-focused { - .Select-control { - border: 0 !important; - height: 32px; - } - } - - .Select-menu-outer { - position: absolute; - width: 330px; - min-width: 125px; - left: -12px; - border-radius: 6px; - } - - .Select-control { - background-color: #fff; - border: 0 !important; - border-radius: none; - position: none; - width: max-content; // move select arrow - height: 20px; - - &:hover { - box-shadow: none; - } - - &:hover .Select-value-label { - color: $core-vibrant-blue !important; - } - - .Select-arrow-zone { - padding-left: $pad-small; - - .Select-arrow { - top: 1px !important; - margin-top: 0 !important; - margin-bottom: 4px; - } - } - - .Select-multi-value-wrapper { - width: max-content; // move select arrow - height: 20px; - margin-bottom: $pad-xsmall; - - .Select-input { - display: none !important; - } - - .Select-value { - position: relative; // move select arrow - display: inline-block; // move select arrow - line-height: 28px; - padding: 0; - border: 0 !important; - background-color: #fff !important; - right: 0; - left: 0; - bottom: 0; - top: 0; - - &.is-focused { - border: 0 !important; - } - :hover { - border: 0 !important; - } - - .Select-value-label { - font-size: $large !important; - } - } - } - } -} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/index.ts b/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/index.ts deleted file mode 100644 index 2006101341..0000000000 --- a/frontend/pages/policies/ManagePoliciesPage/components/TeamsDropdown/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TeamsDropdown"; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 46cd312db3..ffc33f9696 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -209,11 +209,6 @@ const ManageQueriesPage = (): JSX.Element => {

Queries

-
-

- Manage queries to ask specific questions about your devices. -

-
{!isOnlyObserver && !!fleetQueries?.length && ( @@ -228,6 +223,9 @@ const ManageQueriesPage = (): JSX.Element => { )} +
+

Manage queries to ask specific questions about your devices.

+
{isTableDataLoading && !fleetQueriesError && } {!isTableDataLoading && fleetQueriesError ? ( diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index a453d9cf59..db1dd20546 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -1,14 +1,18 @@ .manage-queries-page { &__header-wrap { + height: 38px; display: flex; align-items: center; justify-content: space-between; - margin-bottom: $pad-xxlarge; } &__header { display: flex; align-items: center; + + .form-field { + margin-bottom: 0; + } } &__text { @@ -37,8 +41,7 @@ } &__description { - margin: 0 0 $pad-medium; - padding-top: $pad-xsmall; + margin: 0 0 $pad-xxlarge; h2 { text-transform: uppercase; diff --git a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx index 3615f4caad..a374f47ecc 100644 --- a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx @@ -5,7 +5,6 @@ import { useQuery } from "react-query"; import { useDispatch, useSelector } from "react-redux"; import { AppContext } from "context/app"; import { push } from "react-router-redux"; -import { find } from "lodash"; // @ts-ignore import deepDifference from "utilities/deep_difference"; @@ -20,12 +19,12 @@ import fleetQueriesAPI from "services/entities/queries"; import teamsAPI from "services/entities/teams"; // @ts-ignore import { renderFlash } from "redux/nodes/notifications/actions"; -import permissionUtils from "utilities/permissions"; +import sortUtils from "utilities/sort"; import paths from "router/paths"; import Button from "components/buttons/Button"; // @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; +import TeamsDropdown from "components/TeamsDropdown"; import IconToolTip from "components/IconToolTip"; import TableDataError from "components/TableDataError"; import ScheduleListWrapper from "./components/ScheduleListWrapper"; @@ -117,33 +116,46 @@ interface IFormData { team_id?: number; } -interface ITeamOptions { - disabled: boolean; - label: string; - value: string | number; -} - const ManageSchedulePage = ({ params: { team_id }, - location, }: ITeamSchedulesPageProps): JSX.Element => { const dispatch = useDispatch(); const { MANAGE_PACKS, MANAGE_SCHEDULE, MANAGE_TEAM_SCHEDULE } = paths; const handleAdvanced = () => dispatch(push(MANAGE_PACKS)); - const { - currentUser, - isOnGlobalTeam, - isPremiumTier, - isAnyTeamMaintainerOrTeamAdmin, - } = useContext(AppContext); + const { currentUser, isOnGlobalTeam, isPremiumTier, isFreeTier } = useContext( + AppContext + ); - const { data: teams } = useQuery(["teams"], () => teamsAPI.loadAll({}), { - enabled: !!isPremiumTier, - select: (data) => data.teams, - refetchOnMount: false, - refetchOnWindowFocus: false, - }); + const filterAndSortTeamOptions = (allTeams: ITeam[], userTeams: ITeam[]) => { + const filteredSortedTeams = allTeams + .sort((teamA: ITeam, teamB: ITeam) => + sortUtils.caseInsensitiveAsc(teamA.name, teamB.name) + ) + .filter((team: ITeam) => { + const userTeam = userTeams.find( + (thisUserTeam) => thisUserTeam.id === team.id + ); + return userTeam?.role !== "observer" ? team : null; + }); + + return filteredSortedTeams; + }; + + const { data: teams, isLoading: isLoadingTeams } = useQuery( + ["teams"], + () => teamsAPI.loadAll({}), + { + enabled: !!isPremiumTier, + select: (data) => { + return currentUser?.teams + ? filterAndSortTeamOptions(data.teams, currentUser.teams) + : data.teams; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + } + ); const { data: fleetQueries } = useQuery( ["fleetQueries"], @@ -155,51 +167,18 @@ const ManageSchedulePage = ({ } ); - let teamId = parseInt(team_id, 10); + const teamId = team_id ? parseInt(team_id, 10) : 0; - // isTeamMaintainerOrTeamAdmin set locally and not in AppContext - const isTeamMaintainerOrTeamAdmin = (() => { - return !!permissionUtils.isTeamMaintainerOrTeamAdmin(currentUser, teamId); - })(); - - const onChangeSelectedTeam = (selectedTeamId: number) => { - if (isNaN(selectedTeamId)) { - dispatch(push(MANAGE_SCHEDULE)); - } else { + const handleTeamSelect = (selectedTeamId: number) => { + if (selectedTeamId) { dispatch(push(MANAGE_TEAM_SCHEDULE(selectedTeamId))); + } else { + dispatch(push(MANAGE_SCHEDULE)); } }; - const loadFirstMaintainerOrAdminTeam = () => { - if (currentUser) { - const adminOrMaintainerTeam = currentUser.teams.find((team) => { - return team.role === "admin" || team.role === "maintainer" - ? team.id - : null; - }); - if (adminOrMaintainerTeam) { - teamId = adminOrMaintainerTeam.id; - onChangeSelectedTeam(teamId); - } - } - }; - - if (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin && !teamId) { - loadFirstMaintainerOrAdminTeam(); - } - - if (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin && teamId) { - if (currentUser) { - const canLoadTeam = currentUser.teams.find((team) => { - return ( - (team.role === "admin" || team.role === "maintainer") && - team.id === teamId - ); - }); - if (!canLoadTeam) { - loadFirstMaintainerOrAdminTeam(); - } - } + if (!isOnGlobalTeam && !teamId && teams) { + handleTeamSelect(teams[0].id); } // TODO: move team scheduled queries and global scheduled queries into services entities, remove redux @@ -233,7 +212,7 @@ const ManageSchedulePage = ({ const inheritedQueryOrQueries = allTeamsScheduledQueriesList.length === 1 ? "query" : "queries"; - const selectedTeam = isNaN(teamId) ? "global" : teamId; + const selectedTeam = !teamId ? "global" : teamId; const selectedTeamData = teams?.find((team: ITeam) => selectedTeam === team.id) || undefined; @@ -271,55 +250,6 @@ const ManageSchedulePage = ({ setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal); }, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]); - const generateTeamOptionsDropdownItems = (): ITeamOptions[] => { - const teamOptions: ITeamOptions[] = []; - - if (isAnyTeamMaintainerOrTeamAdmin && currentUser) { - currentUser.teams.forEach((team) => { - if (team.role === "admin" || team.role === "maintainer") { - teamOptions.push({ - disabled: false, - label: team.name, - value: team.id, - }); - } - }); - } else if (isOnGlobalTeam && teams) { - teamOptions.push({ - disabled: false, - label: "All teams", - value: "global", - }); - - teams.forEach((team: ITeam) => { - teamOptions.push({ - disabled: false, - label: team.name, - value: team.id, - }); - }); - } - - return teamOptions; - }; - - const renderTitleOrDropdown = (): JSX.Element => { - const dropDownOptions = generateTeamOptionsDropdownItems(); - return dropDownOptions.length === 1 ? ( -

{dropDownOptions[0].label}

- ) : ( - - onChangeSelectedTeam(newSelectedValue) - } - /> - ); - }; - const onRemoveScheduledQueryClick = ( selectedTableQueryIds: number[] ): void => { @@ -435,57 +365,33 @@ const ManageSchedulePage = ({ [dispatch, teamId, toggleScheduleEditorModal] ); - if (selectedTeam === "global" && isTeamMaintainerOrTeamAdmin) { - const teamMaintainerTeams = generateTeamOptionsDropdownItems(); - if (teamMaintainerTeams.length) { - dispatch( - push(MANAGE_TEAM_SCHEDULE(Number(teamMaintainerTeams[0].value))) - ); - } - } - return (
- {!isPremiumTier ? ( -
-

- Schedule -

-
-

- Schedule recurring queries for your hosts. Fleet’s query - schedule lets you add queries which are executed at regular - intervals. -

-
+
+
+ {isFreeTier &&

Schedule

} + {isPremiumTier && teams && teams.length > 1 && ( + + handleTeamSelect(newSelectedValue) + } + /> + )} + {isPremiumTier && teams && teams.length === 1 && ( +

{teams[0].name}

+ )}
- ) : ( -
- {renderTitleOrDropdown()} -
- {isNaN(teamId) ? ( -

- Schedule queries to run at regular intervals across{" "} - all of your hosts. -

- ) : ( -

- Schedule additional queries for all hosts assigned to this - team. -

- )} -
-
- )} +
- {/* Hide CTA Buttons if no schedule or schedule error */} {allScheduledQueriesList.length !== 0 && allScheduledQueriesError.length !== 0 && (
- {!isTeamMaintainerOrTeamAdmin && ( + {isOnGlobalTeam && (
-
- {renderTable( - onRemoveScheduledQueryClick, - onEditScheduledQueryClick, - allScheduledQueriesList, - allScheduledQueriesError, - toggleScheduleEditorModal, - isOnGlobalTeam || false, - selectedTeamData +
+ {!isLoadingTeams && ( +
+ {!teamId ? ( +

+ Schedule queries to run at regular intervals across{" "} + all of your hosts. +

+ ) : ( +

+ Schedule queries for{" "} + all hosts assigned to this team. +

+ )} +
)}
+
+ {!isLoadingTeams && + renderTable( + onRemoveScheduledQueryClick, + onEditScheduledQueryClick, + allScheduledQueriesList, + allScheduledQueriesError, + toggleScheduleEditorModal, + isOnGlobalTeam || false, + selectedTeamData + )} +
{/* must use ternary for NaN */} {teamId && allTeamsScheduledQueriesList.length > 0 ? ( <> diff --git a/frontend/pages/schedule/ManageSchedulePage/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/_styles.scss index f453fc4abc..72cb0cc5b7 100644 --- a/frontend/pages/schedule/ManageSchedulePage/_styles.scss +++ b/frontend/pages/schedule/ManageSchedulePage/_styles.scss @@ -3,7 +3,7 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: $pad-xxlarge; + height: 38px; } &__header { @@ -42,7 +42,7 @@ &__description { margin: 0; - padding-top: $pad-xsmall; + margin-bottom: $pad-xxlarge; h2 { text-transform: uppercase; @@ -86,95 +86,12 @@ } } - &__team-dropdown { - border: 0 !important; - position: relative; - - :hover { - cursor: pointer !important; - } - - &.is-focused { - .Select-control { - border: 0 !important; - height: 32px; - } - } - - .Select-menu-outer { - position: absolute; - left: -12px; - border-radius: 6px; - } - } - .Select.is-open { .Select-value-label { color: $core-vibrant-blue !important; } } - &__header { - .Select-control { - background-color: #fff; - border: 0 !important; - border-radius: none; - position: none; - width: max-content; // move select arrow - height: 20px; - - &:hover { - box-shadow: none; - } - - &:hover .Select-value-label { - color: $core-vibrant-blue !important; - } - - .Select-arrow-zone { - padding-left: $pad-small; - - .Select-arrow { - top: 1px !important; - margin-top: 0 !important; - } - } - .Select-multi-value-wrapper { - width: max-content; // move select arrow - height: 20px; - margin-bottom: $pad-xsmall; - - .Select-input { - display: none !important; - } - - .Select-value { - position: relative; // move select arrow - display: inline-block; // move select arrow - line-height: 28px; - padding: 0; - border: 0 !important; - background-color: #fff !important; - right: 0; - left: 0; - bottom: 0; - top: 0; - - &.is-focused { - border: 0 !important; - } - :hover { - border: 0 !important; - } - - .Select-value-label { - font-size: $large !important; - } - } - } - } - } - &__inherited-queries-button { margin: $pad-medium 0 0 0; color: $core-vibrant-blue; diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index f4a260f003..aa1c541959 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -49,10 +49,9 @@ a { } .body-wrap { - padding: $pad-xxlarge 30px 0; + padding: $pad-xxlarge; border-radius: 3px; background-color: $core-white; - border: solid 1px $core-white; min-width: 798px; }