mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): show version files diff based on active version (#11686)
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
type QueryOptions,
|
||||
} from "react-query";
|
||||
import { delay } from "utils/delay";
|
||||
import { getTemplateVersionFiles } from "utils/templateVersion";
|
||||
|
||||
export const templateByNameKey = (orgId: string, name: string) => [
|
||||
orgId,
|
||||
@@ -236,6 +237,38 @@ export const resources = (versionId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const templateFiles = (fileId: string) => {
|
||||
return {
|
||||
queryKey: ["templateFiles", fileId],
|
||||
queryFn: async () => {
|
||||
const tarFile = await API.getFile(fileId);
|
||||
return getTemplateVersionFiles(tarFile);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const previousTemplateVersion = (
|
||||
organizationId: string,
|
||||
templateName: string,
|
||||
versionName: string,
|
||||
) => {
|
||||
return {
|
||||
queryKey: [
|
||||
"templateVersion",
|
||||
organizationId,
|
||||
templateName,
|
||||
versionName,
|
||||
"previous",
|
||||
],
|
||||
queryFn: () =>
|
||||
API.getPreviousTemplateVersionByName(
|
||||
organizationId,
|
||||
templateName,
|
||||
versionName,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const waitBuildToBeFinished = async (version: TemplateVersion) => {
|
||||
let data: TemplateVersion;
|
||||
let jobStatus: ProvisionerJobStatus;
|
||||
|
||||
@@ -18,7 +18,7 @@ const meta: Meta<typeof TemplateFiles> = {
|
||||
component: TemplateFiles,
|
||||
args: {
|
||||
currentFiles: exampleFiles,
|
||||
previousFiles: exampleFiles,
|
||||
baseFiles: exampleFiles,
|
||||
tab: { value: "0", set: action("change tab") },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { type FC } from "react";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { DockerIcon } from "components/Icons/DockerIcon";
|
||||
import { MarkdownIcon } from "components/Icons/MarkdownIcon";
|
||||
import { TerraformIcon } from "components/Icons/TerraformIcon";
|
||||
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter";
|
||||
import { UseTabResult } from "hooks/useTab";
|
||||
import { UseTabResult, useTab } from "hooks/useTab";
|
||||
import { AllowedExtension, TemplateVersionFiles } from "utils/templateVersion";
|
||||
import InsertDriveFileOutlined from "@mui/icons-material/InsertDriveFileOutlined";
|
||||
|
||||
@@ -39,19 +39,22 @@ const languageByExtension: Record<AllowedExtension, string> = {
|
||||
|
||||
interface TemplateFilesProps {
|
||||
currentFiles: TemplateVersionFiles;
|
||||
previousFiles?: TemplateVersionFiles;
|
||||
/**
|
||||
* Files used to compare with current files
|
||||
*/
|
||||
baseFiles?: TemplateVersionFiles;
|
||||
tab: UseTabResult;
|
||||
}
|
||||
|
||||
export const TemplateFiles: FC<TemplateFilesProps> = ({
|
||||
currentFiles,
|
||||
previousFiles,
|
||||
baseFiles,
|
||||
tab,
|
||||
}) => {
|
||||
const filenames = Object.keys(currentFiles);
|
||||
const selectedFilename = filenames[Number(tab.value)];
|
||||
const currentFile = currentFiles[selectedFilename];
|
||||
const previousFile = previousFiles && previousFiles[selectedFilename];
|
||||
const previousFile = baseFiles && baseFiles[selectedFilename];
|
||||
|
||||
return (
|
||||
<div css={styles.files}>
|
||||
@@ -61,9 +64,9 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({
|
||||
const extension = getExtension(filename) as AllowedExtension;
|
||||
const icon = iconByExtension[extension];
|
||||
const hasDiff =
|
||||
previousFiles &&
|
||||
previousFiles[filename] &&
|
||||
currentFiles[filename] !== previousFiles[filename];
|
||||
baseFiles &&
|
||||
baseFiles[filename] &&
|
||||
currentFiles[filename] !== baseFiles[filename];
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -93,6 +96,27 @@ export const TemplateFiles: FC<TemplateFilesProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFileTab = (templateFiles: TemplateVersionFiles | undefined) => {
|
||||
// Tabs The default tab is the tab that has main.tf but until we loads the
|
||||
// files and check if main.tf exists we don't know which tab is the default
|
||||
// one so we just use empty string
|
||||
const tab = useTab("file", "");
|
||||
const isLoaded = tab.value !== "";
|
||||
useEffect(() => {
|
||||
if (templateFiles && !isLoaded) {
|
||||
const terraformFileIndex = Object.keys(templateFiles).indexOf("main.tf");
|
||||
// If main.tf exists use the index if not just use the first tab
|
||||
tab.set(terraformFileIndex !== -1 ? terraformFileIndex.toString() : "0");
|
||||
}
|
||||
}, [isLoaded, tab, templateFiles]);
|
||||
|
||||
return {
|
||||
...tab,
|
||||
isLoaded,
|
||||
};
|
||||
};
|
||||
|
||||
const styles = {
|
||||
tabs: (theme) => ({
|
||||
display: "flex",
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { TemplateVersion } from "api/typesGenerated";
|
||||
import { useTab } from "hooks/useTab";
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import {
|
||||
TemplateVersionFiles,
|
||||
getTemplateVersionFiles,
|
||||
} from "utils/templateVersion";
|
||||
import * as API from "api/api";
|
||||
|
||||
export const useFileTab = (templateFiles: TemplateVersionFiles | undefined) => {
|
||||
// Tabs The default tab is the tab that has main.tf but until we loads the
|
||||
// files and check if main.tf exists we don't know which tab is the default
|
||||
// one so we just use empty string
|
||||
const tab = useTab("file", "");
|
||||
const isLoaded = tab.value !== "";
|
||||
useEffect(() => {
|
||||
if (templateFiles && !isLoaded) {
|
||||
const terraformFileIndex = Object.keys(templateFiles).indexOf("main.tf");
|
||||
// If main.tf exists use the index if not just use the first tab
|
||||
tab.set(terraformFileIndex !== -1 ? terraformFileIndex.toString() : "0");
|
||||
}
|
||||
}, [isLoaded, tab, templateFiles]);
|
||||
|
||||
return {
|
||||
...tab,
|
||||
isLoaded,
|
||||
};
|
||||
};
|
||||
|
||||
export const useTemplateFiles = (
|
||||
templateName: string,
|
||||
version: TemplateVersion | undefined,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["templateFiles", templateName, version],
|
||||
queryFn: () => {
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
return getTemplateFilesWithDiff(templateName, version);
|
||||
},
|
||||
enabled: version !== undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const getTemplateFilesWithDiff = async (
|
||||
templateName: string,
|
||||
version: TemplateVersion,
|
||||
) => {
|
||||
const previousVersion = await API.getPreviousTemplateVersionByName(
|
||||
version.organization_id!,
|
||||
templateName,
|
||||
version.name,
|
||||
);
|
||||
const loadFilesPromises: ReturnType<typeof getTemplateVersionFiles>[] = [];
|
||||
loadFilesPromises.push(getTemplateVersionFiles(version.job.file_id));
|
||||
if (previousVersion) {
|
||||
loadFilesPromises.push(
|
||||
getTemplateVersionFiles(previousVersion.job.file_id),
|
||||
);
|
||||
}
|
||||
const [currentFiles, previousFiles] = await Promise.all(loadFilesPromises);
|
||||
return {
|
||||
currentFiles,
|
||||
previousFiles,
|
||||
};
|
||||
};
|
||||
@@ -1,18 +1,30 @@
|
||||
import { type FC } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { TemplateFiles } from "components/TemplateFiles/TemplateFiles";
|
||||
import { useFileTab, useTemplateFiles } from "components/TemplateFiles/hooks";
|
||||
import {
|
||||
TemplateFiles,
|
||||
useFileTab,
|
||||
} from "components/TemplateFiles/TemplateFiles";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import { getTemplatePageTitle } from "../utils";
|
||||
import { useQuery } from "react-query";
|
||||
import { previousTemplateVersion, templateFiles } from "api/queries/templates";
|
||||
import { useOrganizationId } from "hooks";
|
||||
|
||||
const TemplateFilesPage: FC = () => {
|
||||
const orgId = useOrganizationId();
|
||||
const { template, activeVersion } = useTemplateLayoutContext();
|
||||
const { data: templateFiles } = useTemplateFiles(
|
||||
template.name,
|
||||
activeVersion,
|
||||
const { data: currentFiles } = useQuery(
|
||||
templateFiles(activeVersion.job.file_id),
|
||||
);
|
||||
const tab = useFileTab(templateFiles?.currentFiles);
|
||||
const { data: previousTemplate } = useQuery(
|
||||
previousTemplateVersion(orgId, template.name, activeVersion.name),
|
||||
);
|
||||
const { data: previousFiles } = useQuery({
|
||||
...templateFiles(previousTemplate?.job.file_id ?? ""),
|
||||
enabled: Boolean(previousTemplate),
|
||||
});
|
||||
const tab = useFileTab(currentFiles);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -20,10 +32,10 @@ const TemplateFilesPage: FC = () => {
|
||||
<title>{getTemplatePageTitle("Source Code", template)}</title>
|
||||
</Helmet>
|
||||
|
||||
{templateFiles && tab.isLoaded ? (
|
||||
{previousFiles && currentFiles && tab.isLoaded ? (
|
||||
<TemplateFiles
|
||||
currentFiles={templateFiles.currentFiles}
|
||||
previousFiles={templateFiles.previousFiles}
|
||||
currentFiles={currentFiles}
|
||||
baseFiles={previousFiles}
|
||||
tab={tab}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -6,8 +6,13 @@ import { useParams } from "react-router-dom";
|
||||
import { pageTitle } from "utils/page";
|
||||
import TemplateVersionPageView from "./TemplateVersionPageView";
|
||||
import { useQuery } from "react-query";
|
||||
import { templateVersionByName } from "api/queries/templates";
|
||||
import { useFileTab, useTemplateFiles } from "components/TemplateFiles/hooks";
|
||||
import {
|
||||
templateByName,
|
||||
templateFiles,
|
||||
templateVersion,
|
||||
templateVersionByName,
|
||||
} from "api/queries/templates";
|
||||
import { useFileTab } from "components/TemplateFiles/TemplateFiles";
|
||||
|
||||
type Params = {
|
||||
version: string;
|
||||
@@ -18,16 +23,30 @@ export const TemplateVersionPage: FC = () => {
|
||||
const { version: versionName, template: templateName } =
|
||||
useParams() as Params;
|
||||
const orgId = useOrganizationId();
|
||||
const templateVersionQuery = useQuery(
|
||||
|
||||
/**
|
||||
* Template version files
|
||||
*/
|
||||
const templateQuery = useQuery(templateByName(orgId, templateName));
|
||||
const selectedVersionQuery = useQuery(
|
||||
templateVersionByName(orgId, templateName, versionName),
|
||||
);
|
||||
const { data: templateFiles, error: templateFilesError } = useTemplateFiles(
|
||||
templateName,
|
||||
templateVersionQuery.data,
|
||||
);
|
||||
const tab = useFileTab(templateFiles?.currentFiles);
|
||||
const selectedVersionFilesQuery = useQuery({
|
||||
...templateFiles(selectedVersionQuery.data?.job.file_id ?? ""),
|
||||
enabled: Boolean(selectedVersionQuery.data),
|
||||
});
|
||||
const activeVersionQuery = useQuery({
|
||||
...templateVersion(templateQuery.data?.active_version_id ?? ""),
|
||||
enabled: Boolean(templateQuery.data),
|
||||
});
|
||||
const activeVersionFilesQuery = useQuery({
|
||||
...templateFiles(activeVersionQuery.data?.job.file_id ?? ""),
|
||||
enabled: Boolean(activeVersionQuery.data),
|
||||
});
|
||||
const tab = useFileTab(selectedVersionFilesQuery.data);
|
||||
|
||||
const permissions = usePermissions();
|
||||
const versionId = templateVersionQuery.data?.id;
|
||||
const versionId = selectedVersionQuery.data?.id;
|
||||
const createWorkspaceUrl = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (versionId) {
|
||||
@@ -44,10 +63,16 @@ export const TemplateVersionPage: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<TemplateVersionPageView
|
||||
error={templateVersionQuery.error || templateFilesError}
|
||||
currentVersion={templateVersionQuery.data}
|
||||
currentFiles={templateFiles?.currentFiles}
|
||||
previousFiles={templateFiles?.previousFiles}
|
||||
error={
|
||||
templateQuery.error ||
|
||||
selectedVersionQuery.error ||
|
||||
selectedVersionFilesQuery.error ||
|
||||
activeVersionQuery.error ||
|
||||
activeVersionFilesQuery.error
|
||||
}
|
||||
currentVersion={selectedVersionQuery.data}
|
||||
currentFiles={selectedVersionFilesQuery.data}
|
||||
baseFiles={activeVersionFilesQuery.data}
|
||||
versionName={versionName}
|
||||
templateName={templateName}
|
||||
tab={tab}
|
||||
|
||||
@@ -38,7 +38,7 @@ const defaultArgs: TemplateVersionPageViewProps = {
|
||||
"some.tpl": `{{.Name}}`,
|
||||
"some.sh": `echo "Hello world"`,
|
||||
},
|
||||
previousFiles: undefined,
|
||||
baseFiles: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface TemplateVersionPageViewProps {
|
||||
error: unknown;
|
||||
currentVersion: TemplateVersion | undefined;
|
||||
currentFiles: TemplateVersionFiles | undefined;
|
||||
previousFiles: TemplateVersionFiles | undefined;
|
||||
baseFiles: TemplateVersionFiles | undefined;
|
||||
}
|
||||
|
||||
export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
||||
@@ -38,7 +38,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
||||
createWorkspaceUrl,
|
||||
currentVersion,
|
||||
currentFiles,
|
||||
previousFiles,
|
||||
baseFiles,
|
||||
error,
|
||||
}) => {
|
||||
return (
|
||||
@@ -103,7 +103,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
||||
<TemplateFiles
|
||||
tab={tab}
|
||||
currentFiles={currentFiles}
|
||||
previousFiles={previousFiles}
|
||||
baseFiles={baseFiles}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as API from "api/api";
|
||||
import { FileTree, createFile } from "./filetree";
|
||||
import { TarReader } from "./tar";
|
||||
|
||||
@@ -6,10 +5,9 @@ import { TarReader } from "./tar";
|
||||
export type TemplateVersionFiles = Record<string, string>;
|
||||
|
||||
export const getTemplateVersionFiles = async (
|
||||
fileId: string,
|
||||
tarFile: ArrayBuffer,
|
||||
): Promise<TemplateVersionFiles> => {
|
||||
const files: TemplateVersionFiles = {};
|
||||
const tarFile = await API.getFile(fileId);
|
||||
const tarReader = new TarReader();
|
||||
await tarReader.readFile(tarFile);
|
||||
for (const file of tarReader.fileInfo) {
|
||||
|
||||
Reference in New Issue
Block a user