Skip to content

Commit

Permalink
Merge pull request #147 from vbihun/implemented-import-all-functionality
Browse files Browse the repository at this point in the history
Implemented import all functionality
  • Loading branch information
vbihun authored Jan 14, 2025
2 parents 8908660 + 1745428 commit 0ae994a
Show file tree
Hide file tree
Showing 24 changed files with 955 additions and 80 deletions.
1 change: 1 addition & 0 deletions src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum GitlabFeaturesEnum {
DATA_COMPONENT_TYPES = 'isDataComponentTypesEnabled',
DISABLE_DOCUMENT_COMPONENT_LINKS = 'isDocumentComponentLinksDisabled',
ENABLE_GITLAB_MAINTAINER_TOKEN = 'isGitlabMaintainerTokenEnabled',
IMPORT_ALL = 'isImportAllEnabled',
}

export type FeaturesList = { [key in GitlabFeaturesEnum]: boolean };
20 changes: 18 additions & 2 deletions src/resolvers/admin-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ import { setupAndValidateWebhook } from '../services/webhooks';
import { disconnectGroup } from '../services/disconnect-group';
import { getForgeAppId } from '../utils/get-forge-app-id';
import { getLastSyncTime } from '../services/last-sync-time';
import { appId, connectedGroupsInfo, getFeatures, groupsAllExisting, webhookSetupConfig } from './shared-resolvers';
import { ConnectGroupInput, GitLabRoles, WebhookSetupConfig } from '../types';
import {
appId,
connectedGroupsInfo,
getFeatures,
getGroupsProjects,
groupsAllExisting,
importProject,
webhookSetupConfig,
} from './shared-resolvers';
import { ConnectGroupInput, GitLabRoles, GroupProjectsResponse, WebhookSetupConfig } from '../types';

const resolver = new Resolver();

Expand Down Expand Up @@ -114,6 +122,10 @@ resolver.define('groups/allExisting', async (): Promise<ResolverResponse<GitlabA
return groupsAllExisting();
});

resolver.define('groups/projects', async (req): Promise<ResolverResponse<GroupProjectsResponse>> => {
return getGroupsProjects(req);
});

resolver.define('project/lastSyncTime', async (): Promise<ResolverResponse<string | null>> => {
try {
const lastSyncTime = await getLastSyncTime();
Expand All @@ -129,6 +141,10 @@ resolver.define('project/lastSyncTime', async (): Promise<ResolverResponse<strin
}
});

resolver.define('project/import', async (req): Promise<ResolverResponse> => {
return importProject(req);
});

resolver.define('features', (req): ResolverResponse<FeaturesList> => {
const {
context: { cloudId },
Expand Down
56 changes: 11 additions & 45 deletions src/resolvers/import-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ import {
} from '../services/import-projects';
import { GroupProjectsResponse, TeamsWithMembershipStatus, WebhookSetupConfig } from '../types';
import { getAllComponentTypeIds } from '../client/compass';
import { appId, connectedGroupsInfo, getFeatures, groupsAllExisting, webhookSetupConfig } from './shared-resolvers';
import {
appId,
connectedGroupsInfo,
getFeatures,
getGroupsProjects,
groupsAllExisting,
importProject,
webhookSetupConfig,
} from './shared-resolvers';
import { getFirstPageOfTeamsWithMembershipStatus } from '../services/get-teams';
import { getTeamOnboarding, setTeamOnboarding } from '../services/onboarding';

Expand All @@ -34,53 +42,11 @@ resolver.define('groups/allExisting', async (): Promise<ResolverResponse<GitlabA
});

resolver.define('groups/projects', async (req): Promise<ResolverResponse<GroupProjectsResponse>> => {
const {
payload: { groupId, page, groupTokenId, search },
context: { cloudId },
} = req;

try {
const { projects, total } = await getGroupProjects(cloudId, groupId, page, groupTokenId, search);

return { success: true, data: { projects, total } };
} catch (e) {
return {
success: false,
errors: [{ message: e.message, errorType: e.errorType }],
};
}
return getGroupsProjects(req);
});

resolver.define('project/import', async (req): Promise<ResolverResponse> => {
const {
payload: { projectsReadyToImport, groupId },
context: { cloudId },
} = req;

console.log({
message: 'Begin importing projects',
count: projectsReadyToImport.length,
cloudId,
});

try {
await importProjects(cloudId, projectsReadyToImport, groupId);
return {
success: true,
};
} catch (e) {
if (e instanceof ImportFailedError) {
return {
success: false,
errors: [{ message: e.message, errorType: e.errorType }],
};
}

return {
success: false,
errors: [{ message: e.message, errorType: ImportErrorTypes.UNEXPECTED_ERROR }],
};
}
return importProject(req);
});

resolver.define('project/import/status', async (): Promise<ResolverResponse<ImportStatus>> => {
Expand Down
63 changes: 61 additions & 2 deletions src/resolvers/shared-resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { AuthErrorTypes, DefaultErrorTypes, FeaturesList, GitlabAPIGroup, ResolverResponse } from '../resolverTypes';
import {
AuthErrorTypes,
DefaultErrorTypes,
FeaturesList,
GitlabAPIGroup,
ImportErrorTypes,
ResolverResponse,
} from '../resolverTypes';
import { listFeatures } from '../services/feature-flags';
import { getAllExistingGroups, getConnectedGroups } from '../services/group';
import { getWebhookSetupConfig, setupAndValidateWebhook } from '../services/webhooks';
import { getForgeAppId } from '../utils/get-forge-app-id';
import { WebhookSetupConfig } from '../types';
import { GroupProjectsResponse, WebhookSetupConfig } from '../types';
import { getGroupProjects } from '../services/fetch-projects';
import { ImportFailedError, importProjects } from '../services/import-projects';

export const getFeatures = (cloudId: string): ResolverResponse<FeaturesList> => {
try {
Expand Down Expand Up @@ -80,3 +89,53 @@ export const webhookSetupConfig = async (): Promise<ResolverResponse<WebhookSetu
};
}
};

export const getGroupsProjects = async (req: any): Promise<ResolverResponse<GroupProjectsResponse>> => {
const {
payload: { groupId, page, groupTokenId, search, perPage },
context: { cloudId },
} = req;

try {
const { projects, total } = await getGroupProjects(cloudId, groupId, page, groupTokenId, search, perPage);

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

export const importProject = async (req: any): Promise<ResolverResponse> => {
const {
payload: { projectsReadyToImport, groupId },
context: { cloudId },
} = req;

console.log({
message: 'Begin importing projects',
count: projectsReadyToImport.length,
cloudId,
});

try {
await importProjects(cloudId, projectsReadyToImport, groupId);
return {
success: true,
};
} catch (e) {
if (e instanceof ImportFailedError) {
return {
success: false,
errors: [{ message: e.message, errorType: e.errorType }],
};
}

return {
success: false,
errors: [{ message: e.message, errorType: ImportErrorTypes.UNEXPECTED_ERROR }],
};
}
};
5 changes: 5 additions & 0 deletions src/services/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ export const isGitlabMaintainerTokenEnabled = (cloudId?: string, defaultValue =
return (process.env.ENABLE_GITLAB_MAINTAINER_TOKEN === 'true' && isEnabledForCloudId) || defaultValue;
};

const isImportAllEnabled = (defaultValue = false): boolean => {
return process.env.FF_IMPORT_ALL_ENABLED === 'true' || defaultValue;
};

export const listFeatures = (cloudId?: string): FeaturesList => {
return {
[GitlabFeaturesEnum.SEND_STAGING_EVENTS]: isSendStagingEventsEnabled(),
[GitlabFeaturesEnum.DATA_COMPONENT_TYPES]: isDataComponentTypesEnabled(),
[GitlabFeaturesEnum.DISABLE_DOCUMENT_COMPONENT_LINKS]: isDocumentComponentLinksDisabled(),
[GitlabFeaturesEnum.ENABLE_GITLAB_MAINTAINER_TOKEN]: isGitlabMaintainerTokenEnabled(cloudId),
[GitlabFeaturesEnum.IMPORT_ALL]: isImportAllEnabled(),
};
};
9 changes: 6 additions & 3 deletions src/services/fetch-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import { getProjectLabels } from './get-labels';
import { ALL_SETTLED_STATUS, getFormattedErrors, hasRejections } from '../utils/promise-allsettled-helpers';

const PER_PAGE = 10;

const mapComponentLinks = (links: Link[] = []): CreateLinkInput[] =>
links.map((link) => {
return { url: link.url, type: link.type };
Expand All @@ -24,10 +26,10 @@ const fetchProjects = async (
groupId: number,
page: number,
search?: string,
perPage = PER_PAGE,
): Promise<{ total: number; projects: Project[] }> => {
try {
const PER_PAGE = 10;
const { data: projects, headers } = await getProjects(groupToken, groupId, page, PER_PAGE, search);
const { data: projects, headers } = await getProjects(groupToken, groupId, page, perPage, search);

const generatedProjectsWithLanguagesResult = await Promise.allSettled(
projects.map(async (project) => {
Expand Down Expand Up @@ -142,11 +144,12 @@ export const getGroupProjects = async (
page: number,
groupTokenId: number,
search?: string,
perPage?: number,
): Promise<GroupProjectsResponse> => {
try {
const groupToken = await storage.getSecret(`${STORAGE_SECRETS.GROUP_TOKEN_KEY_PREFIX}${groupTokenId}`);

const { projects, total } = await fetchProjects(groupToken, groupId, page, search);
const { projects, total } = await fetchProjects(groupToken, groupId, page, search, perPage);

const checkedDataWithExistingComponentsResults = await Promise.allSettled(
projects.map(({ id: projectId }) => {
Expand Down
3 changes: 3 additions & 0 deletions src/utils/create-compass-yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ describe('formatLinks', () => {
isSendStagingEventsEnabled: false,
isDocumentComponentLinksDisabled: true,
isGitlabMaintainerTokenEnabled: false,
isImportAllEnabled: false,
});

const result = formatLinks(inputLinks);
Expand All @@ -244,6 +245,7 @@ describe('formatLinks', () => {
isSendStagingEventsEnabled: false,
isDocumentComponentLinksDisabled: true,
isGitlabMaintainerTokenEnabled: false,
isImportAllEnabled: false,
});

const result = formatLinks(null);
Expand All @@ -257,6 +259,7 @@ describe('formatLinks', () => {
isSendStagingEventsEnabled: false,
isDocumentComponentLinksDisabled: false,
isGitlabMaintainerTokenEnabled: false,
isImportAllEnabled: false,
});

const result = formatLinks(inputLinks);
Expand Down
2 changes: 2 additions & 0 deletions ui/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ImportProgressResultPage } from './components/ImportProgressResultPage'
import { useAppContext } from './hooks/useAppContext';
import { ApplicationState, ROUTES } from './routes';
import { IMPORT_MODULE_KEY } from './constants';
import { ImportAllPage } from './components/ImportAll';

export const AppRouter = () => {
const { initialRoute, moduleKey } = useAppContext();
Expand All @@ -29,6 +30,7 @@ export const AppRouter = () => {
<Routes>
<Route {...ROUTES[ApplicationState.AUTH]} element={<AuthPage />} />
<Route {...ROUTES[ApplicationState.CONNECTED]} element={<ConnectedPage />} />
<Route {...ROUTES.importAll} element={<ImportAllPage />} />
<Route {...ROUTES.Import} element={<SelectImportPage />} />
<Route {...ROUTES.ImportProgress} element={<ImportProgressResultPage moduleKey={moduleKey} />} />
</Routes>
Expand Down
74 changes: 58 additions & 16 deletions ui/src/components/ConnectedPage/ImportControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,42 @@ import Spinner from '@atlaskit/spinner';
import InlineMessage from '@atlaskit/inline-message';
import { router } from '@forge/bridge';

import { getCallBridge } from '@forge/bridge/out/bridge';
import { useNavigate } from 'react-router-dom';
import { ImportProgressBar } from '../ImportProgressBar';
import { useImportContext } from '../../hooks/useImportContext';
import { getLastSyncTime } from '../../services/invokes';
import { formatLastSyncTime } from '../../helpers/time';
import { ImportButtonWrapper } from '../styles';
import { ImportButtonWrapper, LastSyncTimeWrapper, StartImportButtonWrapper } from '../styles';
import { useAppContext } from '../../hooks/useAppContext';
import { Separator } from '../TooltipGenerator/styles';
import { ApplicationState } from '../../routes';

export const ImportControls = () => {
const [lastSyncTime, setLastSyncTime] = useState<string | null>(null);
const [lastSyncTimeIsLoading, setLastSyncTimeIsLoading] = useState<boolean>(false);
const [lastSyncTimeErrorMessage, setLastSyncTimeAnErrorMessage] = useState<string>();

const { isImportInProgress } = useImportContext();
const { appId } = useAppContext();
const { appId, features } = useAppContext();
const navigate = useNavigate();

const handleImportNavigate = async () => {
await router.navigate(`/compass/import/redirect/${encodeURIComponent(`ari:cloud:ecosystem::app/${appId}`)}`);
};

const handleImportAllButton = async () => {
const actionSubject = 'importAllButton';
const action = 'clicked';

await getCallBridge()('fireForgeAnalytic', {
forgeAppId: appId,
analyticEvent: `${actionSubject} ${action}`,
});

navigate(`${ApplicationState.CONNECTED}/import-all`, { replace: true });
};

const fetchLastSyncTime = async () => {
setLastSyncTimeIsLoading(true);

Expand Down Expand Up @@ -59,21 +76,46 @@ export const ImportControls = () => {
{isImportInProgress ? (
<ImportProgressBar />
) : (
<ImportButtonWrapper>
<Button appearance='primary' onClick={handleImportNavigate}>
Import
</Button>

{lastSyncTimeIsLoading && <Spinner data-testid='loading-spinner' />}
{lastSyncTimeErrorMessage && (
<InlineMessage testId='error-message' type='error' title={`Can't get last imported time`}>
<p>{lastSyncTimeErrorMessage}</p>
</InlineMessage>
)}
{!lastSyncTimeIsLoading && !lastSyncTimeErrorMessage && (
<time data-testid='last-import-time'>{lastSyncTimeMsg}</time>
<>
{features.isImportAllEnabled && (
<StartImportButtonWrapper>
<ImportButtonWrapper shouldShowImportAll={Boolean(features.isImportAllEnabled)}>
<Button
shouldFitContainer
testId='import-all-repositories-btn'
appearance='primary'
onClick={handleImportAllButton}
>
Import all repositories
</Button>
</ImportButtonWrapper>
</StartImportButtonWrapper>
)}
</ImportButtonWrapper>
{features.isImportAllEnabled && <Separator />}
<StartImportButtonWrapper>
<ImportButtonWrapper shouldShowImportAll={Boolean(features.isImportAllEnabled)}>
<Button
shouldFitContainer
testId='import-repositories-btn'
appearance={features.isImportAllEnabled ? 'default' : 'primary'}
onClick={handleImportNavigate}
>
Import
</Button>
</ImportButtonWrapper>
</StartImportButtonWrapper>
<LastSyncTimeWrapper>
{lastSyncTimeIsLoading && <Spinner data-testid='loading-spinner' />}
{lastSyncTimeErrorMessage && (
<InlineMessage testId='error-message' type='error' title={`Can't get last imported time`}>
<p>{lastSyncTimeErrorMessage}</p>
</InlineMessage>
)}
{!lastSyncTimeIsLoading && !lastSyncTimeErrorMessage && (
<time data-testid='last-import-time'>{lastSyncTimeMsg}</time>
)}
</LastSyncTimeWrapper>
</>
)}
</>
);
Expand Down
Loading

0 comments on commit 0ae994a

Please sign in to comment.