Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/app/api/github/download/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// src/app/api/github/download/route.ts
'use server';
import { NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import { GITHUB_API_URL } from '@/types/const';
import { getGitHubUsername } from '@/utils/github';

const UPSTREAM_REPO_NAME = process.env.NEXT_PUBLIC_TAXONOMY_REPO!;

export async function POST(req: NextRequest) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET! });

if (!token || !token.accessToken) {
console.error('Unauthorized: Missing or invalid access token');
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const githubToken = token.accessToken as string;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${githubToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};

const githubUsername = await getGitHubUsername(headers);
try {
const { branchName } = await req.json();

if (!branchName || typeof branchName !== 'string') {
return NextResponse.json({ error: 'contribution branch does not exist on remote taxonomy.' }, { status: 400 });
}

const tarballUrl = `${GITHUB_API_URL}/repos/${githubUsername}/${UPSTREAM_REPO_NAME}/tarball/${branchName}`;
const tarballRes = await fetch(tarballUrl, {
headers: headers
});

if (!tarballRes.ok) {
return NextResponse.json({ error: 'Failed to download taxonomy for the contribution.' }, { status: 500 });
}

return new NextResponse(tarballRes.body, {
headers: {
'Content-Type': 'application/gzip',
'Content-Disposition': `attachment`,
'Cache-Control': 'no-store'
}
});
} catch (error) {
console.error('failed to download taxonomy for the contribution:', error);
return NextResponse.json({ error: error }, { status: 500 });
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
// src/app/api/download/route.ts
// src/app/api/native/download/route.ts
'use server';
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import * as git from 'isomorphic-git';

// GET handler now takes the Request so we can watch for aborts
export async function GET(request: Request) {
const rootDir = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR;
const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`;

export async function POST(req: NextRequest) {
const rootDir = LOCAL_TAXONOMY_ROOT_DIR;
if (!rootDir) {
return NextResponse.json({ error: 'NEXT_PUBLIC_TAXONOMY_ROOT_DIR is not configured' }, { status: 500 });
return NextResponse.json({ error: 'Failed to find the local taxonomy that contains the contribution.' }, { status: 500 });
}
const { branchName } = await req.json();

const taxonomyDir = path.join(rootDir, 'taxonomy');
try {
await fs.promises.access(taxonomyDir, fs.constants.R_OK);
} catch {
return NextResponse.json({ error: 'Taxonomy directory not found or not readable' }, { status: 404 });
return NextResponse.json({ error: 'Taxonomy directory not found or not readable' }, { status: 500 });
}

// Checkout the new branch
await git.checkout({ fs, dir: taxonomyDir, ref: branchName });

// Spawn tar to write gzipped archive to stdout
const tar = spawn('tar', ['-czf', '-', '-C', rootDir, 'taxonomy'], {
stdio: ['ignore', 'pipe', 'inherit']
});

// If the client aborts, make sure to kill the tar process
request.signal.addEventListener('abort', () => {
req.signal.addEventListener('abort', () => {
tar.kill('SIGTERM');
});

Expand All @@ -51,7 +57,7 @@ export async function GET(request: Request) {
status: 200,
headers: {
'Content-Type': 'application/gzip',
'Content-Disposition': 'attachment; filename="taxonomy.tar.gz"',
'Content-Disposition': `attachment`,
'Cache-Control': 'no-store'
}
});
Expand Down
23 changes: 22 additions & 1 deletion src/components/Dashboard/Github/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as React from 'react';
import { useSession } from 'next-auth/react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { fetchPullRequests, getGitHubUsername } from '../../../utils/github';
import { DraftEditFormInfo, PullRequest } from '@/types';
import { useState } from 'react';
import {
Expand Down Expand Up @@ -40,6 +39,8 @@ import {
} from '@patternfly/react-core';
import { ExternalLinkAltIcon, OutlinedQuestionCircleIcon, GithubIcon, EllipsisVIcon, PficonTemplateIcon } from '@patternfly/react-icons';
import { deleteDraftData, fetchDraftContributions } from '@/components/Contribute/Utils/autoSaveUtils';
import { handleTaxonomyDownload } from '@/utils/taxonomy';
import { fetchPullRequests, getGitHubUsername } from '@/utils/github';

const InstructLabLogo: React.FC = () => <Image src="/InstructLab-LogoFile-RGB-FullColor.svg" alt="InstructLab Logo" width={256} height={256} />;

Expand All @@ -48,6 +49,7 @@ const DashboardGithub: React.FunctionComponent = () => {
const [pullRequests, setPullRequests] = React.useState<PullRequest[]>([]);
const [draftContributions, setDraftContributions] = React.useState<DraftEditFormInfo[]>([]);
const [isFirstPullDone, setIsFirstPullDone] = React.useState<boolean>(false);
const [isDownloadDone, setIsDownloadDone] = React.useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(true);
//const [error, setError] = React.useState<string | null>(null);
const [isActionMenuOpen, setIsActionMenuOpen] = React.useState<{ [key: number | string]: boolean }>({});
Expand Down Expand Up @@ -195,6 +197,16 @@ const DashboardGithub: React.FunctionComponent = () => {
</ModalBody>
</Modal>
)}
{!isDownloadDone && (
<Modal variant={ModalVariant.small} title="Retrieving taxonomy tar file" isOpen onClose={() => setIsDownloadDone(true)}>
<ModalBody>
<div>
<Spinner size="md" />
Retrieving the taxonomy compressed file with the contributed data.
</div>
</ModalBody>
</Modal>
)}
{isFirstPullDone && pullRequests.length === 0 && draftContributions.length === 0 ? (
<EmptyState titleText="Welcome to InstructLab" headingLevel="h4" icon={InstructLabLogo}>
<EmptyStateBody>
Expand Down Expand Up @@ -347,6 +359,15 @@ const DashboardGithub: React.FunctionComponent = () => {
</DropdownItem>
)}
</DropdownList>
<DropdownItem
key="download-taxonomy"
onClick={() => {
setIsDownloadDone(false);
handleTaxonomyDownload({ branchName: pr.head.ref, isGithubMode: true, setIsDownloadDone });
}}
>
Download taxonomy
</DropdownItem>
</Dropdown>
)
}}
Expand Down
17 changes: 15 additions & 2 deletions src/components/Dashboard/Native/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/Ex
import { v4 as uuidv4 } from 'uuid';
import { DraftEditFormInfo } from '@/types';
import { deleteDraftData, fetchDraftContributions } from '@/components/Contribute/Utils/autoSaveUtils';
import { handleTaxonomyDownload } from '@/utils/taxonomy';

const InstructLabLogo: React.FC = () => <Image src="/InstructLab-LogoFile-RGB-FullColor.svg" alt="InstructLab Logo" width={256} height={256} />;

Expand Down Expand Up @@ -79,6 +80,7 @@ const DashboardNative: React.FunctionComponent = () => {
const [selectedDraftContribution, setSelectedDraftContribution] = React.useState<string | null>(null);
const [isPublishing, setIsPublishing] = React.useState(false);
const [expandedFiles, setExpandedFiles] = React.useState<Record<string, boolean>>({});
const [isDownloadDone, setIsDownloadDone] = React.useState<boolean>(true);

const router = useRouter();

Expand Down Expand Up @@ -393,6 +395,17 @@ const DashboardNative: React.FunctionComponent = () => {
))}
</AlertGroup>

{!isDownloadDone && (
<Modal variant={ModalVariant.small} title="Retrieving taxonomy tar file" isOpen onClose={() => setIsDownloadDone(true)}>
<ModalBody>
<div>
<Spinner size="md" />
Retrieving the taxonomy compressed file with the contributed data.
</div>
</ModalBody>
</Modal>
)}

{isLoading ? (
<Spinner size="lg" />
) : branches.length === 0 && draftContributions.length === 0 ? (
Expand Down Expand Up @@ -546,8 +559,8 @@ const DashboardNative: React.FunctionComponent = () => {
<DropdownItem
key="download-taxonomy"
onClick={() => {
// this will trigger the browser download
window.location.href = '/api/download';
setIsDownloadDone(false);
handleTaxonomyDownload({ branchName: branch.name, isGithubMode: false, setIsDownloadDone });
}}
>
Download taxonomy
Expand Down
33 changes: 33 additions & 0 deletions src/utils/taxonomy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// src/utils/taxonomy.ts
const GITHUB_TAXONOMY_DOWNLOAD_URL = '/api/github/download';
const NATIVE_TAXONOMY_DOWNLOAD_URL = '/api/native/download';

interface TaxonomyDownloadProp {
branchName: string;
isGithubMode: boolean;
setIsDownloadDone: (isDownloadDone: boolean) => void;
}
export async function handleTaxonomyDownload({ branchName, isGithubMode, setIsDownloadDone }: TaxonomyDownloadProp) {
const res = await fetch(isGithubMode ? GITHUB_TAXONOMY_DOWNLOAD_URL : NATIVE_TAXONOMY_DOWNLOAD_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
branchName: branchName
})
});

if (!res.ok) {
alert('Failed to download the taxonomy');
return { message: res.statusText, status: res.status };
}

setIsDownloadDone(true);

const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `taxonomy-${branchName}.tar.gz`;
a.click();
window.URL.revokeObjectURL(url);
}