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
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default function AddConfigurationModal({
onClick={handleClickedOutside}
>
<div className="bg-background border-border relative h-100 w-1/3 min-w-200 rounded-lg border p-6 shadow-lg">
<h2 className="mb-4 text-lg font-semibold">Add Configuration</h2>
<h2 className="mb-4 text-lg font-semibold">Add File</h2>
<p className="mb-4">Add a new configuration file.</p>

<div className="mb-4 flex items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function ConfigurationFileTile({
return (
<div className="border-border bg-background relative flex h-75 w-100 flex-col rounded border p-4 shadow-sm">
{/* Header */}
<div className="text-foreground mb-3 truncate text-sm font-semibold" title={relativePath}>
<div className="text-foreground text-md mb-3 truncate font-semibold" title={relativePath}>
{relativePath}
</div>

Expand All @@ -55,9 +55,9 @@ export default function ConfigurationFileTile({
{adapterNames.length > 0 ? (
<>
<h1 className="text-foreground mb-2 text-xs">
Adapter{adapterNames.length == 1 ? '' : 's'} within this configuration:
Adapter{adapterNames.length == 1 ? '' : 's'} within this file
</h1>
<div className="bg-backdrop border-border flex-1 overflow-y-auto rounded border p-2">
<div className="border-border flex-1 overflow-y-auto rounded border p-2 shadow-inner">
<ul className="space-y-2">
{adapterNames.map((name, index) => (
<AdapterListItem
Expand Down Expand Up @@ -101,7 +101,7 @@ interface AdapterListItemProps {

function AdapterListItem({ name, adapterPosition, onOpenInStudio }: AdapterListItemProps) {
return (
<li className="border-border bg-background flex items-center rounded border px-2 py-1">
<li className="border-border bg-background flex items-center rounded border px-2 py-1 shadow-sm">
{/* Adapter name – 2/3 */}
<span className="text-foreground border-border w-2/3 truncate border-r text-xs" title={name}>
{name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { FileTreeNode } from '~/types/filesystem.types'
import { fetchProjectTree } from '~/services/file-tree-service'
import Button from '~/components/inputs/button'
import Search from '~/components/search/search'
import { toRelativePath } from '~/utils/path-utils'
import { normalizePath, toRelativePath } from '~/utils/path-utils'

interface ConfigurationFile {
path: string
Expand All @@ -22,7 +22,7 @@ interface ConfigurationFile {
function findConfigurationsDir(node: FileTreeNode | undefined | null): FileTreeNode | null {
if (!node || !node.path) return null

const normalizedPath = node.path.replaceAll('\\', '/')
const normalizedPath = normalizePath(node.path)

if (node.type === 'DIRECTORY' && normalizedPath.endsWith(`/src/main/configurations/${node.name}`)) {
return node
Expand Down Expand Up @@ -117,7 +117,7 @@ export default function ConfigurationOverview() {

const xmlFiles = collectXmlFiles(configurationDirectory)
return xmlFiles.map((file) => {
const relativePath = toRelativePath(file.path, 'src/main/configurations/') ?? file.name
const relativePath = toRelativePath(file.path, `${configurationDirectory.path}/`) ?? file.name
return { ...file, relativePath, path: file.path }
})
}, [tree, currentConfigurationProject])
Expand Down Expand Up @@ -185,13 +185,12 @@ export default function ConfigurationOverview() {
<h1 className="ml-2 text-2xl font-bold">Configuration Overview</h1>
<div className="mb-4 flex items-center justify-between">
<p className="ml-2">
Configuration files within src/main/configurations/
<span className="font-bold">{currentConfigurationProject.name}</span>:
Configuration files within <span className="font-bold">{currentConfigurationProject.name}</span>
</p>
<Search value={searchQuery} onChange={handleSearch} />
</div>

<div className="border-border bg-background flex flex-wrap gap-4 self-start rounded border p-4">
<div className="border-border bg-background flex flex-wrap gap-4 self-start">
{filteredConfigurationFiles.map((file) => (
<ConfigurationFileTile
key={file.path}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface NewProjectModalProperties {
isLocal: boolean
onClose: () => void
onCreate: (name: string, rootPath: string) => void
initialPath?: string
initialPath: string
}

const CONFIG_DIR = 'src/main/configurations'
Expand All @@ -26,12 +26,12 @@ export default function NewConfigurationModal({

useEffect(() => {
if (!isOpen || !isLocal) {
if (isOpen) setLocation(initialPath ?? '')
if (isOpen) setLocation(initialPath)
return
}

filesystemService
.resolveNearestAccessiblePath(initialPath ?? '')
.resolveNearestAccessiblePath(initialPath)
.then(setLocation)
.catch(() => setLocation(''))
}, [isOpen, isLocal, initialPath])
Expand Down
13 changes: 7 additions & 6 deletions src/main/frontend/app/routes/projectlanding/project-landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { useProjectStore } from '~/stores/project-store'
import { ApiError } from '~/utils/api'
import { logApiError } from '~/utils/logger'
import {getParentPath, normalizePath} from '~/utils/path-utils'

Check warning on line 9 in src/main/frontend/app/routes/projectlanding/project-landing.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Replace `getParentPath,·normalizePath` with `·getParentPath,·normalizePath·`

import ConfigurationRow from './configuration-row'
import Search from '~/components/search/search'
Expand Down Expand Up @@ -187,7 +188,7 @@
const projects = recentProjects ?? []
const filteredProjects = projects.filter((project) => project.name.toLowerCase().includes(searchTerm.toLowerCase()))

const lastRecentRootPath = projects[0]?.rootPath
const lastRecentRootPath = normalizePath(projects[0]?.rootPath ?? '')

if (isLoading || isOpeningProject) return <LoadingState />

Expand Down Expand Up @@ -231,14 +232,14 @@
onClose={() => setIsModalOpen(false)}
onCreate={onCreateProject}
isLocal={isLocalEnvironment}
initialPath={lastRecentRootPath}
initialPath={getParentPath(lastRecentRootPath)}
/>
<CloneConfigurationModal
isOpen={isCloneModalOpen}
isLocal={isLocalEnvironment}
onClose={() => setIsCloneModalOpen(false)}
onClone={onCloneProject}
initialPath={lastRecentRootPath}
initialPath={getParentPath(lastRecentRootPath)}
/>
{!isLocalEnvironment && (
<input
Expand All @@ -256,7 +257,7 @@
onSelect={onOpenFolder}
onCancel={() => setIsOpenPickerOpen(false)}
rootLabel={rootLocationName}
initialPath={lastRecentRootPath}
initialPath={getParentPath(lastRecentRootPath)}
/>
</div>
)
Expand Down Expand Up @@ -351,13 +352,13 @@
</section>
)

const Toolbar = ({ onSearchChange }: { onSearchChange: (val: string) => void }) => (
const Toolbar = ({ onSearchChange }: { onSearchChange: (value: string) => void }) => (
<div className="border-border flex h-12 border-b">
<div className="border-border flex w-1/4 min-w-50 items-center border-r px-4 text-xs font-bold tracking-wider text-slate-500 uppercase">
<ArchiveIcon className="mr-2 h-4 w-4" /> Recent
</div>
<div className="flex flex-1 items-center px-4">
<Search onChange={(e) => onSearchChange(e.target.value)} />
<Search onChange={(event) => onSearchChange(event.target.value)} />
</div>
</div>
)
Expand Down
33 changes: 10 additions & 23 deletions src/main/frontend/app/utils/path-utils.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
import type { ConfigurationProject } from '~/types/project.types'

/**
* Extracts the portion of a path after the first occurrence of a marker segment.
* Returns null if the marker is not found.
*/
export function toRelativePath(absolutePath: string, marker: string): string | null {
const normalized = absolutePath.replaceAll('\\', '/')
const idx = normalized.indexOf(marker)
return idx === -1 ? null : normalized.slice(idx + marker.length)
const normalizedPath = normalizePath(absolutePath)
const normalizedMarker = normalizePath(marker)
const idx = normalizedPath.indexOf(marker)
return idx === -1 ? null : normalizedMarker.slice(idx + marker.length)
}

/**
* Converts a file path to a project-relative path for display.
* Handles both local and cloud environments
*/
export function toProjectRelativePath(absolutePath: string, project: ConfigurationProject): string {
const path = absolutePath.replaceAll('\\', '/')
const root = project.rootPath.replaceAll('\\', '/').replace(/^\/+/, '').replace(/\/+$/, '')
const normalizedPath = path.replace(/^\/+/, '')

if (normalizedPath === root) {
return `${project.name}/`
}

const relative = toRelativePath(normalizedPath, `${root}/`)
if (relative !== null) {
return `${project.name}/${relative}`
}
export function normalizePath(path: string) {
return path.replaceAll('\\', '/')
}

return path
export function getParentPath(path: string): string {
if (!path) return path // Return empty string if path is empty, small optimization to avoid regex processing
return path.replace(/\/?[^/]*$/, '')
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public ResponseEntity<ConfigurationProjectDTO> cloneProject(@RequestBody Configu
}

@PostMapping("/open")
public ResponseEntity<ConfigurationProjectDTO> openProject(@RequestBody ConfigurationProjectCreateDTO configurationProjectCreateDTO) throws IOException, ApiException {
public ResponseEntity<ConfigurationProjectDTO> openProject(@RequestBody ConfigurationProjectCreateDTO configurationProjectCreateDTO)
throws IOException, ApiException {
ConfigurationProject configurationProject = configurationProjectService.openProjectFromDisk(configurationProjectCreateDTO.rootPath());
recentProjectsService.addRecentProject(configurationProject.getName(), configurationProject.getRootPath());
return ResponseEntity.ok(configurationProjectService.toDto(configurationProject));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@
@Log4j2
@Service
public class ConfigurationProjectService {
private static final String CONFIGURATIONS_DIR = "src/main/configurations";

private final FileSystemStorage fileSystemStorage;
private final RecentProjectsService recentProjectsService;

Expand Down Expand Up @@ -70,7 +68,7 @@ private List<ConfigurationProject> getProjectsFromRecentList() {
ConfigurationProject configurationProject = loadProjectCached(recent.rootPath());
foundProjects.add(configurationProject);
} catch (Exception _) {
log.debug("Recent project no longer valid: {}", recent.rootPath());
log.debug("Recent project is no longer valid: {}", recent.rootPath());
}
}
return foundProjects;
Expand Down Expand Up @@ -101,13 +99,16 @@ public ConfigurationProject getProject(String name) throws ApiException {
return getProjects().stream()
.filter(project -> project.getName().equals(name))
.findFirst()
.orElseThrow(() -> new ApiException("Project not found: " + name, HttpStatus.NOT_FOUND));
.orElseThrow(() -> new ApiException("Project \"" + name +"\" not found", HttpStatus.NOT_FOUND));
}

public ConfigurationProject createProjectOnDisk(ConfigurationProjectCreateDTO projectCreate) throws IOException {
Path rootPath = Path.of(projectCreate.rootPath());
String resolvedRootPath = rootPath.endsWith(CONFIGURATIONS_DIR) ? projectCreate.name() : CONFIGURATIONS_DIR + "/" + projectCreate.name();
Path projectCreationPath = rootPath.resolve(resolvedRootPath);
Path projectCreationPath = rootPath.resolve(projectCreate.name());

if (Files.exists(projectCreationPath)) {
throw new ApiException("Project already exists at " + projectCreationPath, HttpStatus.NOT_FOUND);
}
Path projectPath = fileSystemStorage.createProjectDirectory(projectCreationPath.toString());

ClassPathResource resource = new ClassPathResource("templates/default-configuration.xml");
Expand All @@ -125,7 +126,9 @@ public ConfigurationProject createProjectOnDisk(ConfigurationProjectCreateDTO pr
public ConfigurationProject openProjectFromDisk(String path) throws IOException, ApiException {
Path absolutePath = fileSystemStorage.toAbsolutePath(path);
if (!Files.exists(absolutePath) || !Files.isDirectory(absolutePath)) {
throw new ApiException("Project not found at: " + path, HttpStatus.NOT_FOUND);
throw new ApiException("Project not found at \"" + path + "\"", HttpStatus.NOT_FOUND);
} else if (!absolutePath.resolve("Configuration.xml").toFile().exists()) {
throw new ApiException("Project doesn't seem to be a valid configuration or Configuration.xml might be missing", HttpStatus.BAD_REQUEST);
}
return loadProjectAndCache(path);
}
Expand All @@ -134,7 +137,7 @@ public ConfigurationProject cloneAndOpenProject(String repoUrl, String localPath
Path targetDir = fileSystemStorage.toAbsolutePath(localPath);

if (Files.exists(targetDir)) {
throw new IllegalArgumentException("Project already exists at: " + localPath);
throw new IllegalArgumentException("Project already exists at \"" + localPath + "\"");
}

try {
Expand All @@ -152,9 +155,9 @@ public ConfigurationProject cloneAndOpenProject(String repoUrl, String localPath
} catch (GitAPIException exception) {
String msg = exception.getMessage() != null ? exception.getMessage().toLowerCase() : "";
if (msg.contains("auth") || msg.contains("not permitted") || msg.contains("403") || msg.contains("401")) {
throw new IllegalArgumentException("Clone failed — authentication error. Please provide a valid Personal Access Token (PAT)", exception);
throw new IllegalArgumentException("Cloning authentication error. Please provide a valid Personal Access Token (PAT)", exception);
}
throw new IllegalArgumentException("Clone failed: " + exception.getMessage(), exception);
throw new IllegalArgumentException("Cloning failed: " + exception.getMessage(), exception);
}

ConfigurationProject configurationProject = loadProjectAndCache(targetDir.toString());
Expand Down Expand Up @@ -239,8 +242,7 @@ public ConfigurationProjectDTO toDto(ConfigurationProject configurationProject)
Path absolutePath = fileSystemStorage.toAbsolutePath(configurationProject.getRootPath());
boolean isGitRepo = Files.isDirectory(absolutePath.resolve(".git"));

boolean hasStoredToken =
configurationProject.getGitToken() != null && !configurationProject.getGitToken().isBlank();
boolean hasStoredToken = configurationProject.getGitToken() != null && !configurationProject.getGitToken().isBlank();

return new ConfigurationProjectDTO(
configurationProject.getName(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,9 @@ void testOpenProjectFromDisk() throws Exception {

String projectName = "manual_project";
Path projectDir = tempDir.resolve(projectName);
Files.createDirectories(projectDir.resolve("src/main/configurations"));
Files.createDirectories(projectDir);
Files.writeString(
projectDir.resolve("src/main/configurations/TestConfig.xml"),
projectDir.resolve("Configuration.xml"),
"<Configuration name='TestConfig'/>",
StandardCharsets.UTF_8
);
Expand Down Expand Up @@ -449,26 +449,6 @@ void testOpenProjectFromDiskThrowsWhenPathIsAFile() throws Exception {
assertThrows(ApiException.class, () -> configurationProjectService.openProjectFromDisk(file.toString()));
}

@Test
void testOpenProjectFromDiskLoadsEmptyProject_whenNoConfigurationsDir() throws Exception {
when(fileSystemStorage.toAbsolutePath(anyString())).thenAnswer(invocation -> {
String pathStr = invocation.getArgument(0);
Path path = Path.of(pathStr);
return path.isAbsolute() ? path : tempDir.resolve(pathStr);
});

Path projDir = tempDir.resolve("empty_proj");
Files.createDirectory(projDir);

ConfigurationProject configurationProject = configurationProjectService.openProjectFromDisk(projDir.toString());

assertNotNull(configurationProject);
assertEquals("empty_proj", configurationProject.getName());

ConfigurationProjectDTO dto = configurationProjectService.toDto(configurationProject);
assertTrue(dto.filepaths().isEmpty(), "No configurations dir means empty config list");
}

@Test
void testGetProjectsFromWorkspaceScan() throws Exception {
when(fileSystemStorage.isLocalEnvironment()).thenReturn(false);
Expand Down
Loading