Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0bf844a
feat: recently accessed components
vani-walvekar1494 Jun 17, 2025
7a7c3b2
fix: format files to pass nx format check
vani-walvekar1494 Jun 17, 2025
6292b87
Fix: resolved merge conflicts and updated dashboard styles
vani-walvekar1494 Jun 17, 2025
35f37ad
chore: format AppsSideNav to pass format:check
vani-walvekar1494 Jun 17, 2025
3ccaa74
Fix: Replace 'any' with proper types in RecentProjects to satisfy lin…
vani-walvekar1494 Jun 17, 2025
9a6aa1b
chore: format files to pass nx format check
vani-walvekar1494 Jun 17, 2025
2fb2441
wip: partial DB connection setup and fix for recent tools issue
vani-walvekar1494 Jun 23, 2025
c439559
wip: partial DB connection setup and fix for recent tools issue
vani-walvekar1494 Jun 23, 2025
4ec4659
Merge remote-tracking branch 'origin/feat/react-dashboard' into feat/…
vani-walvekar1494 Jun 24, 2025
89f43e8
Apply local changes after merging feat/react-dashboard
vani-walvekar1494 Jun 24, 2025
ae7e243
Fix lint error and other updates
vani-walvekar1494 Jun 27, 2025
9698ff4
Fix lint error and update files after merging feat/react-dashboard
vani-walvekar1494 Jun 27, 2025
c4e91a4
Fix formatting issues
vani-walvekar1494 Jun 27, 2025
98b6f96
Format migration files with Black
vani-walvekar1494 Jun 27, 2025
db1aec1
chore: refactor dashboard API, cleanup migrations, remove unused icons
vani-walvekar1494 Jun 30, 2025
b5e6a15
fix: format files to pass lint checks
vani-walvekar1494 Jul 1, 2025
b5d0868
fix(AppsSideNav): add Applications header text to resolve failing test
vani-walvekar1494 Jul 1, 2025
b6045cd
fix: resolve AppsSideNav formatting issues
vani-walvekar1494 Jul 1, 2025
245cefe
fix: remove unused imports and variables in AppsSideNav to pass lint …
vani-walvekar1494 Jul 1, 2025
7af4852
Merge feat/react-dashboard into feat/react-dashboard-utils: resolved …
vani-walvekar1494 Jul 2, 2025
03e9fa5
feat(dashboard): update layout and fix recent projects visibility
vani-walvekar1494 Jul 11, 2025
aaeed75
Resolved merge conflicts and merged feat/react-dashboard into feat/re…
vani-walvekar1494 Jul 11, 2025
be8befc
Revert package-lock.json changes and formatting changes in api/datafi…
vani-walvekar1494 Jul 17, 2025
913da5a
changed sidebar styling and fixed tool navigation
vani-walvekar1494 Jul 17, 2025
294abbe
fixed linting for package-lock.json
vani-walvekar1494 Jul 17, 2025
6092a55
Fixed AppsSideNav hook usage
vani-walvekar1494 Jul 17, 2025
d23e6ef
fix: show Applications: header during loading to fix unit test
vani-walvekar1494 Jul 17, 2025
e582a90
Refactored favorites logic, fix types,updated styling and dependencies
vani-walvekar1494 Jul 25, 2025
63701fd
Restore apps.ts from main to include missing comments/docstrings
vani-walvekar1494 Jul 25, 2025
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
104 changes: 104 additions & 0 deletions client/modules/_hooks/src/favouritesApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from './apiClient';

export interface FavoriteTool {
tool_id: string;
version?: string;
}

const fetchFavorites = async ({
signal,
}: {
signal: AbortSignal;
}): Promise<FavoriteTool[]> => {
const res = await apiClient.get('/api/workspace/user-favorites/', { signal });
return res.data;
};

const addFavorite = async (toolId: string): Promise<void> => {
await apiClient.post('/api/workspace/user-favorites/', { tool_id: toolId });
};

const removeFavorite = async (toolId: string): Promise<void> => {
await apiClient.post('/api/workspace/user-favorites/remove/', {
tool_id: toolId,
});
};

export const useFavorites = () => {
return useQuery({
queryKey: ['workspace', 'favorites'],
queryFn: fetchFavorites,
staleTime: 5 * 60 * 1000,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
};

export const useAddFavorite = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: addFavorite,
onMutate: async (toolId) => {
await queryClient.cancelQueries({ queryKey: ['workspace', 'favorites'] });

const previousFavorites = queryClient.getQueryData<FavoriteTool[]>([
'workspace',
'favorites',
]);

queryClient.setQueryData<FavoriteTool[]>(
['workspace', 'favorites'],
(old = []) => [...old, { tool_id: toolId }]
);

return { previousFavorites };
},
onError: (_err, _toolId, context) => {
if (context?.previousFavorites) {
queryClient.setQueryData(
['workspace', 'favorites'],
context.previousFavorites
);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workspace', 'favorites'] });
},
});
};

export const useRemoveFavorite = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: removeFavorite,
onMutate: async (toolId) => {
await queryClient.cancelQueries({ queryKey: ['workspace', 'favorites'] });

const previousFavorites = queryClient.getQueryData<FavoriteTool[]>([
'workspace',
'favorites',
]);

queryClient.setQueryData<FavoriteTool[]>(
['workspace', 'favorites'],
(old = []) => old.filter((fav) => fav.tool_id !== toolId)
);

return { previousFavorites };
},
onError: (_err, _toolId, context) => {
if (context?.previousFavorites) {
queryClient.setQueryData(
['workspace', 'favorites'],
context.previousFavorites
);
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workspace', 'favorites'] });
},
});
};
1 change: 1 addition & 0 deletions client/modules/_hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './datafiles';
export * from './systems';
export * from './notifications';
export * from './onboarding';
export * from './favouritesApi';
62 changes: 55 additions & 7 deletions client/modules/dashboard/src/Dashboard/Dashboard.module.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,55 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
.dashboardContainer {
display: flex;
min-height: 100vh;
background-color: #fafafa;
overflow: hidden;
}

.sidebar {
width: 260px;
background-color: #f5f7fa;
padding: 1.5rem;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
position: sticky;
top: 80px;
border-radius: 8px;
min-height: calc(100vh - 80px);
overflow-y: auto;
}

.sidebarMargin {
flex: 1;
padding: 1rem 2rem;
overflow-y: auto;
min-height: calc(100vh - 80px);
}

.sidebarTitle {
font-size: 1.55rem;
font-weight: 600;
margin-bottom: 1.2rem;
color: #2c3e50;
border-bottom: 1px solid #ddd;
padding-bottom: 0.4rem;
}

.sidebarLink {
display: block;
margin: 0.75rem 0;
color: #1f2d3d;
font-size: 1.5rem;
font-weight: 500;
transition: all 0.2s ease;
text-decoration: none;
}

.sidebarLink:hover {
color: #007bff;
text-decoration: underline;
padding-left: 5px;
}

.sidebarIcon {
margin-right: 8px;
vertical-align: middle;
}
36 changes: 30 additions & 6 deletions client/modules/dashboard/src/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Collapse, Typography } from 'antd';
import Quicklinks from './QuickLinksNavbar';
import RecentlyAccessed from './RecentlyAccessed';
import RecentProjects from './RecentProjects';
import { TicketList } from './TicketList';
import styles from './Dashboard.module.css';

/* eslint-disable-next-line */
export interface DashboardProps {}
const { Text } = Typography;
const { Panel } = Collapse;
const queryClient = new QueryClient();

export function Dashboard(props: DashboardProps) {
export function Dashboard() {
return (
<div className={styles['container']}>
<h1>Welcome to Dashboard!</h1>
</div>
<QueryClientProvider client={queryClient}>
<div className={styles.dashboardContainer}>
<div className={styles.sidebar}>
<Quicklinks />
</div>
<div className={styles.sidebarMargin}>
<Collapse ghost expandIconPosition="end">
<Panel header={<Text strong>Recent Projects</Text>} key="1">
<RecentProjects />
</Panel>
<Panel header={<Text strong>Recently Accessed Tools</Text>} key="2">
<RecentlyAccessed />
</Panel>
<Panel header={<Text strong>My Tickets</Text>} key="3">
<TicketList />
</Panel>
</Collapse>
</div>
</div>
</QueryClientProvider>
);
}

Expand Down
158 changes: 158 additions & 0 deletions client/modules/dashboard/src/Dashboard/FavoriteTools.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping in mind what Jake said about module boundaries, I think this should be refactored to use existing schema and types, similar to how useAppsListing and useGetAppParams work. You probably don't need to reinvent the wheel with a tool_id and version when app_id and version already exist.

Again keeping in mind what Jake said about module boundaries, I think you also can probably handle some of these functions with existing code. Does handleClickOutside need to be used when you can use code similar to PreviewModal?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated code now uses existing app_id and version types consistently, it also removes the unnecessary handleClickOutside logic, relying on React state and Ant Design components for UI control.

Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { useState } from 'react';
import { Typography, List, Button } from 'antd';
import { useFavorites, useRemoveFavorite, useAppsListing } from '@client/hooks';
import type { TPortalApp } from '@client/hooks';

const { Link, Text } = Typography;

interface Favorite {
app_id: string;
version?: string;
id: string;
}

interface RecentTool {
label: string;
path: string;
}

const parseToolId = (tool_id: string): { app_id: string; version?: string } => {
const parts = tool_id.split('-');
if (parts.length > 1 && /^\d+(\.\d+)*$/.test(parts[parts.length - 1])) {
return {
app_id: parts.slice(0, -1).join('-'),
version: parts[parts.length - 1],
};
}
return { app_id: tool_id };
};

const makeFavoriteKey = (fav: Favorite) =>
fav.version ? `${fav.app_id}-${fav.version}` : fav.app_id;

const QuickLinksMenu: React.FC = () => {
const [showFavorites, setShowFavorites] = useState(false);
const { data: favoritesData, isLoading: loadingFavs } = useFavorites();
const { data: appsData, isLoading: loadingApps } = useAppsListing();
const removeFavoriteMutation = useRemoveFavorite();
const [removingIds, setRemovingIds] = useState<Set<string>>(new Set());

if (loadingFavs || loadingApps) return <div>Loading favorites...</div>;

const allApps: TPortalApp[] =
appsData?.categories.flatMap((cat) => cat.apps) ?? [];

const favorites: Favorite[] = (favoritesData ?? []).map((fav) => {
const { app_id, version } = parseToolId(fav.tool_id);
return { app_id, version, id: fav.tool_id };
});

const resolvedFavorites = favorites
.map((fav) => {
const matchedApp = allApps.find(
(app) =>
app.app_id === fav.app_id &&
(!fav.version || app.version === fav.version)
);
if (!matchedApp) return null;
return {
key: makeFavoriteKey(fav),
app: matchedApp,
id: fav.id,
};
})
.filter(Boolean) as { key: string; app: TPortalApp; id: string }[];

const handleRemove = async (key: string) => {
if (removingIds.has(key)) return;

setRemovingIds((prev) => new Set(prev).add(key));

try {
await removeFavoriteMutation.mutateAsync(key);
} catch (error) {
console.error('Failed to remove favorite:', error);
} finally {
setRemovingIds((prev) => {
const newSet = new Set(prev);
newSet.delete(key);
return newSet;
});
}
};

const handleToolClick = (app: TPortalApp) => {
const href = app.version
? `/workspace/${app.app_id}?appVersion=${app.version}`
: `/workspace/${app.app_id}`;

const recent: RecentTool[] = JSON.parse(
localStorage.getItem('recentTools') ?? '[]'
);
const updated = [
{ label: app.label, path: href },
...recent.filter((r) => r.path !== href),
].slice(0, 5);
localStorage.setItem('recentTools', JSON.stringify(updated));

window.open(href, '_blank', 'noopener,noreferrer');
};

return (
<nav>
<div style={{ marginBottom: 8 }}>
<Link
onClick={() => setShowFavorites((v) => !v)}
style={{ fontWeight: 'bold', cursor: 'pointer' }}
aria-expanded={showFavorites}
aria-controls="favorite-apps-list"
>
Favorite Apps
</Link>
</div>

{showFavorites && (
<div id="favorite-apps-list" style={{ marginBottom: 16 }}>
{resolvedFavorites.length === 0 ? (
<Text type="secondary">No favorite tools yet.</Text>
) : (
<List
size="small"
dataSource={resolvedFavorites}
bordered
style={{ background: '#fff' }}
renderItem={({ app, key }) => (
<List.Item
key={key}
actions={[
<Button
type="text"
loading={removingIds.has(key)}
onClick={() => handleRemove(key)}
aria-label={`Remove favorite ${app.label}`}
style={{ color: '#FAAD14' }}
>
</Button>,
]}
>
<Link
onClick={() => handleToolClick(app)}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter') handleToolClick(app);
}}
>
{app.label}
</Link>
</List.Item>
)}
/>
)}
</div>
)}
</nav>
);
};

export default QuickLinksMenu;
Loading
Loading