diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index 8c28d0938..5a90cf2b1 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -14,6 +14,7 @@ export type Workspace = {
orgId: string;
version: ProjectVersion;
upgradeStatus: string | null;
+ updatedAt: string;
autoCapitalization: boolean;
environments: WorkspaceEnv[];
pitVersionLimit: number;
diff --git a/frontend/src/lib/fn/date.ts b/frontend/src/lib/fn/date.ts
new file mode 100644
index 000000000..66423b3cb
--- /dev/null
+++ b/frontend/src/lib/fn/date.ts
@@ -0,0 +1,46 @@
+export const timeAgo = (inputDate: Date, currentDate: Date): string => {
+ const now = new Date(currentDate).getTime();
+ const date = new Date(inputDate).getTime();
+ const elapsedMilliseconds = now - date;
+ const elapsedSeconds = Math.abs(Math.floor(elapsedMilliseconds / 1000));
+ const elapsedMinutes = Math.abs(Math.floor(elapsedSeconds / 60));
+ const elapsedHours = Math.abs(Math.floor(elapsedMinutes / 60));
+ const elapsedDays = Math.abs(Math.floor(elapsedHours / 24));
+ const elapsedWeeks = Math.abs(Math.floor(elapsedDays / 7));
+ const elapsedMonths = Math.abs(Math.floor(elapsedDays / 30));
+ const elapsedYears = Math.abs(Math.floor(elapsedDays / 365));
+
+ if (elapsedYears > 0) {
+ return `${elapsedYears} year${elapsedYears === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+ }
+ if (elapsedMonths > 0) {
+ return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+ }
+ if (elapsedWeeks > 0) {
+ return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+ }
+ if (elapsedDays > 0) {
+ return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+ }
+ if (elapsedHours > 0) {
+ return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+ }
+ if (elapsedMinutes > 0) {
+ return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+ }
+ return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${
+ elapsedMilliseconds >= 0 ? "ago" : "from now"
+ }`;
+};
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx
index 17cf8537c..177a6586b 100644
--- a/frontend/src/pages/org/[id]/overview/index.tsx
+++ b/frontend/src/pages/org/[id]/overview/index.tsx
@@ -12,11 +12,14 @@ import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
import {
faArrowRight,
faArrowUpRightFromSquare,
+ faBorderAll,
faCheck,
faCheckCircle,
faClipboard,
faExclamationCircle,
+ faFileShield,
faHandPeace,
+ faList,
faMagnifyingGlass,
faNetworkWired,
faPlug,
@@ -35,6 +38,7 @@ import {
Button,
Checkbox,
FormControl,
+ IconButton,
Input,
Modal,
ModalContent,
@@ -86,6 +90,11 @@ type ItemProps = {
link?: string;
};
+enum ProjectsViewMode {
+ GRID = "grid",
+ LIST = "list"
+}
+
function copyToClipboard(id: string, setState: (value: boolean) => void) {
// Get the text field
const copyText = document.getElementById(id) as HTMLInputElement;
@@ -309,8 +318,9 @@ const LearningItem = ({
href={link}
>
null}
@@ -321,10 +331,11 @@ const LearningItem = ({
await registerUserAction.mutateAsync(userAction);
}
}}
- className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete
+ className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
+ complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
- } text-mineshaft-100 duration-200`}
+ } text-mineshaft-100 duration-200`}
>
@@ -402,8 +413,9 @@ const LearningItemSquare = ({
href={link}
>
null}
@@ -414,10 +426,11 @@ const LearningItemSquare = ({
await registerUserAction.mutateAsync(userAction);
}
}}
- className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete
+ className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${
+ complete
? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700"
: "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700"
- } text-mineshaft-100 duration-200`}
+ } text-mineshaft-100 duration-200`}
>
@@ -431,8 +444,9 @@ const LearningItemSquare = ({
)}
{complete ? "Complete!" : `About ${time}`}
@@ -461,7 +475,6 @@ const formSchema = yup.object({
type TAddProjectFormData = yup.InferType
;
// #TODO: Update all the workspaceIds
-
const OrganizationPage = withPermission(
() => {
const { t } = useTranslation();
@@ -496,6 +509,9 @@ const OrganizationPage = withPermission(
const createWs = useCreateWorkspace();
const { user } = useUser();
const { data: serverDetails } = useFetchServerStatus();
+ const [projectsViewMode, setProjectsViewMode] = useState(
+ (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
+ );
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
// type check
@@ -550,6 +566,95 @@ const OrganizationPage = withPermission(
}, []);
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
+ const filteredWorkspaces = orgWorkspaces.filter((ws) =>
+ ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())
+ );
+
+ const projectsGridView = (
+
+ {isWorkspaceLoading &&
+ Array.apply(0, Array(3)).map((_x, i) => (
+
+ ))}
+ {filteredWorkspaces.map((workspace) => (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
+
{
+ router.push(`/project/${workspace.id}/secrets/overview`);
+ localStorage.setItem("projectData.id", workspace.id);
+ }}
+ key={workspace.id}
+ className="min-w-72 group flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
+ >
+
{workspace.name}
+
+ {workspace.environments?.length || 0} environments
+
+
+
+ ))}
+
+ );
+
+ const projectsListView = (
+
+ {isWorkspaceLoading &&
+ Array.apply(0, Array(3)).map((_x, i) => (
+
+
+
+ ))}
+ {filteredWorkspaces.map((workspace, ind) => (
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
+
{
+ router.push(`/project/${workspace.id}/secrets/overview`);
+ localStorage.setItem("projectData.id", workspace.id);
+ }}
+ key={workspace.id}
+ className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
+ ind === 0 && "rounded-t-md"
+ } ${ind === filteredWorkspaces.length - 1 && "rounded-b-md border-b"}`}
+ >
+
+
+
+ {workspace.environments?.length || 0} environments
+
+
+
+ ))}
+
+ );
return (
@@ -580,7 +685,9 @@ const OrganizationPage = withPermission(
)}
-
Projects
+
setSearchFilter(e.target.value)}
leftIcon={
}
/>
+
+ {
+ localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID);
+ setProjectsViewMode(ProjectsViewMode.GRID);
+ }}
+ ariaLabel="grid"
+ size="xs"
+ className={`${
+ projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent"
+ } min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
+ >
+
+
+ {
+ localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST);
+ setProjectsViewMode(ProjectsViewMode.LIST);
+ }}
+ ariaLabel="list"
+ size="xs"
+ className={`${
+ projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent"
+ } min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
+ >
+
+
+
{(isAllowed) => (
-
- {isWorkspaceLoading &&
- Array.apply(0, Array(3)).map((_x, i) => (
-
- ))}
- {orgWorkspaces
- .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
- .map((workspace) => (
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
-
{
- router.push(`/project/${workspace.id}/secrets/overview`);
- localStorage.setItem("projectData.id", workspace.id);
- }}
- key={workspace.id}
- className="min-w-72 group flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
- >
-
{workspace.name}
-
- {workspace.environments?.length || 0} environments
-
-
-
- ))}
-
+ {projectsViewMode === ProjectsViewMode.LIST ? projectsListView : projectsGridView}
{isWorkspaceEmpty && (
- Onboarding Guide
-
-
- {orgWorkspaces.length !== 0 && (
- <>
-
-
- >
- )}
-
-
-
-
+
+
Onboarding Guide
+
+
{orgWorkspaces.length !== 0 && (
-
-
-
-
- {false && (
-
-
-
- )}
-
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative.
-
+ <>
+
+
+ >
+ )}
+
+
+
+
+ {orgWorkspaces.length !== 0 && (
+
+
+
+
+ {false && (
+
+
+
+ )}
+
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative.
-
- About 2 min
-
-
- {false &&
}
+
+ About 2 min
+
- )}
- {orgWorkspaces.length !== 0 && (
-
- )}
-
- )}
+
+ {false &&
}
+
+ )}
+ {orgWorkspaces.length !== 0 && (
+
+ )}
+
+ )}
{
diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx
index 188d81c65..40b841e0f 100644
--- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx
+++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx
@@ -70,6 +70,7 @@ export const MultiEnvProjectPermission = ({
}, [allRule]);
const handlePermissionChange = (val: Permission) => {
+ if(!val) return
switch (val) {
case Permission.NoAccess: {
const permissions = getValue("permissions");
@@ -106,7 +107,7 @@ export const MultiEnvProjectPermission = ({
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
(selectedPermissionCategory !== Permission.NoAccess || isCustom) &&
- "border-l-2 border-primary-600"
+ "border-l-2 border-primary-600"
)}
>
diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx
index 03fa41b68..8d1fb1db3 100644
--- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx
+++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx
@@ -53,6 +53,7 @@ export const SecretRollbackPermission = ({ isNonEditable, setValue, control }: P
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
+ if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx
index adfae81b1..763aa32f4 100644
--- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx
+++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx
@@ -100,6 +100,7 @@ export const SingleProjectPermission = ({
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
+ if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx
index 9b04c5236..c13a6abde 100644
--- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx
+++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx
@@ -52,6 +52,7 @@ export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props)
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
+ if(!val) return;
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();