Skip to content

Commit

Permalink
Merge pull request #102 from vbihun/feature/team-onboarding-spotlight
Browse files Browse the repository at this point in the history
Team onboarding spotlight
  • Loading branch information
vbihun authored Apr 30, 2024
2 parents 5a70a71 + 577e1f4 commit d986529
Show file tree
Hide file tree
Showing 13 changed files with 586 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const STORAGE_KEYS = {
CURRENT_IMPORT_TOTAL_PROJECTS: 'currentImportTotalProjects',
CURRENT_IMPORT_QUEUE_JOB_IDS: 'currentImportQueueJobIds',
CURRENT_IMPORT_FAILED_PROJECT_PREFIX: 'currentImportFailedProject-',
TEAM_ONBOARDING: 'isTeamOnboardingCompleted',
};

export const STORAGE_SECRETS = {
Expand Down
36 changes: 36 additions & 0 deletions src/resolvers/import-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { GroupProjectsResponse, TeamsWithMembershipStatus } from '../types';
import { getAllComponentTypeIds } from '../client/compass';
import { appId, connectedGroupsInfo, getFeatures, groupsAllExisting } from './shared-resolvers';
import { getFirstPageOfTeamsWithMembershipStatus } from '../services/get-teams';
import { getTeamOnboarding, setTeamOnboarding } from '../services/onboarding';

const resolver = new Resolver();

Expand Down Expand Up @@ -159,4 +160,39 @@ resolver.define(
},
);

resolver.define(
'onboarding/team/get',
async (req): Promise<ResolverResponse<{ isTeamOnboardingCompleted: boolean }>> => {
const { accountId } = req.context;

try {
const isTeamOnboardingCompleted = await getTeamOnboarding(accountId);

return { success: true, data: { isTeamOnboardingCompleted } };
} catch (e) {
return {
success: false,
errors: [{ message: e.message }],
};
}
},
);

resolver.define('onboarding/team/set', async (req): Promise<ResolverResponse> => {
const { accountId } = req.context;

try {
await setTeamOnboarding(accountId);

return {
success: true,
};
} catch (e) {
return {
success: false,
errors: [{ message: e.message }],
};
}
});

export default resolver.getDefinitions();
46 changes: 46 additions & 0 deletions src/services/onboarding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable import/order */
import { storage, mockForgeApi } from '../__tests__/helpers/forge-helper';
/* eslint-disable import/first */

mockForgeApi();

import { STORAGE_KEYS } from '../constants';
import { getTeamOnboarding, setTeamOnboarding } from './onboarding';

const accountId = 'test-account-id';

describe('getTeamOnboarding', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('returns true in case if onboarding is completed', async () => {
storage.get.mockResolvedValue(true);

const result = await getTeamOnboarding(accountId);

expect(result).toEqual(true);
expect(storage.get).toBeCalledWith(`${STORAGE_KEYS.TEAM_ONBOARDING}:${accountId}`);
});

it('returns false in case if onboarding is not completed', async () => {
storage.get.mockResolvedValue(false);

const result = await getTeamOnboarding(accountId);

expect(result).toEqual(false);
expect(storage.get).toBeCalledWith(`${STORAGE_KEYS.TEAM_ONBOARDING}:${accountId}`);
});
});

describe('setTeamOnboarding', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('save to the storage onboarding value', async () => {
await setTeamOnboarding(accountId);

expect(storage.set).toBeCalledWith(`${STORAGE_KEYS.TEAM_ONBOARDING}:${accountId}`, true);
});
});
12 changes: 12 additions & 0 deletions src/services/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { storage } from '@forge/api';
import { STORAGE_KEYS } from '../constants';

export const getTeamOnboarding = async (accountId: string): Promise<boolean> => {
const isTeamOnboardingCompleted = await storage.get(`${STORAGE_KEYS.TEAM_ONBOARDING}:${accountId}`);

return isTeamOnboardingCompleted ?? false;
};

export const setTeamOnboarding = async (accountId: string): Promise<void> => {
await storage.set(`isTeamOnboardingCompleted:${accountId}`, true);
};
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@atlaskit/form": "^8.5.4",
"@atlaskit/icon": "^21.10.6",
"@atlaskit/inline-message": "^11.4.9",
"@atlaskit/onboarding": "^11.3.0",
"@atlaskit/progress-bar": "^0.5.6",
"@atlaskit/section-message": "^6.1.10",
"@atlaskit/select": "^15.2.11",
Expand Down
45 changes: 43 additions & 2 deletions ui/src/components/ProjectsImportTable/buildTableHead.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import Checkbox from '@atlaskit/checkbox';
import { HeadType } from '@atlaskit/dynamic-table/dist/types/types';
import { Spotlight, SpotlightManager, SpotlightTarget, SpotlightTransition } from '@atlaskit/onboarding';
import { N0 } from '@atlaskit/theme/colors';
import { token } from '@atlaskit/tokens';

import { TooltipGenerator } from '../TooltipGenerator';
import { tooltipsText } from '../utils';
import { StatusWrapper } from '../SelectImportPage/styles';
import { ProjectImportSelection } from '../../services/types';
import { OwnerTeamHeadWrapper } from './styles';

type Params = {
projects: ProjectImportSelection[];
onSelectAllItems: (filteredProjects: ProjectImportSelection[], isAllItemsSelected: boolean) => void;
isAllItemsSelected: boolean;
isLoading: boolean;
isSpotlightActive: boolean;
finishOnboarding: () => void;
};

export const buildTableHead = ({ isLoading, onSelectAllItems, isAllItemsSelected, projects }: Params): HeadType => {
export const buildTableHead = ({
isLoading,
onSelectAllItems,
isAllItemsSelected,
projects,
isSpotlightActive,
finishOnboarding,
}: Params): HeadType => {
return {
cells: [
{
Expand Down Expand Up @@ -69,7 +82,35 @@ export const buildTableHead = ({ isLoading, onSelectAllItems, isAllItemsSelected
},
{
key: 'OWNER_TEAM',
content: 'Owner team',
content: (
<SpotlightManager blanketIsTinted={false}>
<SpotlightTarget name='teamonboarding'>
<OwnerTeamHeadWrapper>Owner team</OwnerTeamHeadWrapper>
</SpotlightTarget>
<SpotlightTransition>
{isSpotlightActive && !isLoading && (
<Spotlight
actions={[
{
onClick: finishOnboarding,
text: 'Okay',
},
]}
dialogWidth={315}
heading='Select an owner team'
target='teamonboarding'
key='teamonboarding'
dialogPlacement='left top'
targetRadius={4}
targetBgColor={token('color.icon.inverse', N0)}
>
Select an owner team for the components you import and meet the criteria for your Service Readiness
scorecard.
</Spotlight>
)}
</SpotlightTransition>
</SpotlightManager>
),
width: 15,
},
],
Expand Down
6 changes: 6 additions & 0 deletions ui/src/components/ProjectsImportTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Props = {
importableComponentTypes: ComponentTypesResult;
teamsResult: TeamsForImportResult;
selectProjectTeam: (id: number, ownerTeamOption: SelectOwnerTeamOption | null) => void;
isSpotlightActive: boolean;
finishOnboarding: () => void;
};

const SPINNER_SIZE = 'large';
Expand All @@ -33,6 +35,8 @@ export const ProjectsImportTable = ({
importableComponentTypes,
teamsResult,
selectProjectTeam,
isSpotlightActive,
finishOnboarding,
}: Props) => {
const emptyView = useMemo(() => buildEmptyView({ isProjectsExist: projects.length !== 0, error }), [projects, error]);

Expand All @@ -51,6 +55,8 @@ export const ProjectsImportTable = ({
onSelectAllItems,
isAllItemsSelected,
isLoading,
isSpotlightActive,
finishOnboarding,
})}
rows={buildTableBody({
projects,
Expand Down
5 changes: 5 additions & 0 deletions ui/src/components/ProjectsImportTable/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export const DropdownWrapper = styled.div`
position: relative;
overflow: visible;
`;

export const OwnerTeamHeadWrapper = styled.div`
height: 25px;
padding: 4px 8px;
`;
35 changes: 33 additions & 2 deletions ui/src/components/SelectImportPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { router } from '@forge/bridge';

import { CompassComponentTypeOption, ProjectImportSelection } from '../../services/types';
import { ApplicationState } from '../../routes';
import { getAllExistingGroups, getGroupProjects, importProjects } from '../../services/invokes';
import {
getAllExistingGroups,
getGroupProjects,
getTeamOnboarding,
importProjects,
setTeamOnboarding,
} from '../../services/invokes';
import { ImportableProject, ResolverResponse, GitlabAPIGroup } from '../../resolverTypes';
import { useImportContext } from '../../hooks/useImportContext';
import { SelectProjectsScreen } from './screens/SelectProjectsScreen';
Expand Down Expand Up @@ -50,6 +56,28 @@ export const SelectImportPage = () => {
const [groupId, setGroupId] = useState<number>(DEFAULT_GROUP_ID);
const [groups, setGroups] = useState<GitlabAPIGroup[]>([]);
const [search, setSearch] = useState<string>();
const [isSpotlightActive, setIsSpotlightActive] = useState(false);

const startOnboarding = () => {
getTeamOnboarding()
.then(({ data, success, errors }) => {
if (success && !data?.isTeamOnboardingCompleted) {
setIsSpotlightActive(true);
}

if (errors?.length) {
throw new Error(errors[0].message);
}
})
.catch((e) => {
console.error(e);
});
};

const finishOnboarding = useCallback(() => {
setIsSpotlightActive(false);
setTeamOnboarding();
}, []);

const { changedProjects, setChangedProjects } = useProjects(projects);

Expand Down Expand Up @@ -127,6 +155,7 @@ export const SelectImportPage = () => {

useEffect(() => {
fetchGroups();
startOnboarding();
}, []);

const onSelectAllItems = (filteredProjects: ProjectImportSelection[], isAllItemsSelected: boolean) => {
Expand Down Expand Up @@ -294,6 +323,8 @@ export const SelectImportPage = () => {
importableComponentTypes={importableComponentTypes}
teamsResult={teamsResult}
selectProjectTeam={onSelectProjectTeam}
isSpotlightActive={isSpotlightActive}
finishOnboarding={finishOnboarding}
/>
)}
{screen === Screens.CONFIRMATION && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type Props = {
importableComponentTypes: ComponentTypesResult;
teamsResult: TeamsForImportResult;
selectProjectTeam: (id: number, ownerTeamOption: SelectOwnerTeamOption | null) => void;
isSpotlightActive: boolean;
finishOnboarding: () => void;
};

export const SelectProjectsScreen = ({
Expand All @@ -64,6 +66,8 @@ export const SelectProjectsScreen = ({
importableComponentTypes,
teamsResult,
selectProjectTeam,
isSpotlightActive,
finishOnboarding,
}: Props) => {
const groupSelectorOptions = useMemo(
() => buildGroupsSelectorOptions(groups, locationGroupId),
Expand Down Expand Up @@ -106,6 +110,8 @@ export const SelectProjectsScreen = ({
importableComponentTypes={importableComponentTypes}
teamsResult={teamsResult}
selectProjectTeam={selectProjectTeam}
isSpotlightActive={isSpotlightActive}
finishOnboarding={finishOnboarding}
/>
{projects.length !== 0 ? (
<CenterWrapper>
Expand Down
11 changes: 10 additions & 1 deletion ui/src/components/styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';

import { h400 } from '@atlaskit/theme/typography';
import { N200, N90, N900 } from '@atlaskit/theme/colors';
Expand Down Expand Up @@ -57,10 +57,19 @@ export const ImportButtonWrapper = styled.div`
}
`;

const disableLastHeaderCellStylingForSpotlight = css`
th:last-child {
padding: 0;
}
`;

export const TableWrapper = styled.div`
margin-top: ${gridSize() * 4}px;
max-height: 60vh;
overflow: auto;
padding-right: 12px;
${disableLastHeaderCellStylingForSpotlight}
`;

export const IncomingWebhookSectionWrapper = styled.div`
Expand Down
8 changes: 8 additions & 0 deletions ui/src/services/invokes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,11 @@ export const getFirstPageOfTeamsWithMembershipStatus = (
searchTeamValue,
});
};

export const getTeamOnboarding = (): Promise<ResolverResponse<{ isTeamOnboardingCompleted: boolean }>> => {
return invoke<ResolverResponse<{ isTeamOnboardingCompleted: boolean }>>('onboarding/team/get');
};

export const setTeamOnboarding = (): Promise<ResolverResponse> => {
return invoke<ResolverResponse>('onboarding/team/set');
};
Loading

0 comments on commit d986529

Please sign in to comment.