diff --git a/web/sdk/react/assets/inbox-stack.svg b/web/sdk/react/assets/inbox-stack.svg new file mode 100644 index 000000000..e7584171f --- /dev/null +++ b/web/sdk/react/assets/inbox-stack.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/sdk/react/assets/user-minus.svg b/web/sdk/react/assets/user-minus.svg new file mode 100644 index 000000000..f22dd6321 --- /dev/null +++ b/web/sdk/react/assets/user-minus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/sdk/react/hooks/useOrganizationProjects.ts b/web/sdk/react/hooks/useOrganizationProjects.ts index bed23543b..85567e48b 100644 --- a/web/sdk/react/hooks/useOrganizationProjects.ts +++ b/web/sdk/react/hooks/useOrganizationProjects.ts @@ -12,6 +12,7 @@ interface useOrganizationProjectsProps { export interface UseOrganizationProjectsReturn { isFetching: boolean; + isFetched: boolean; projects: Project[]; userAccessOnProject: Record; refetch: () => void; @@ -25,33 +26,35 @@ export const useOrganizationProjects = ({ const { activeOrganization: organization } = useFrontier(); // Query for organization projects (all projects) - const { - data: orgProjectsData, - isLoading: isOrgProjectsLoading, + const { + data: orgProjectsData, + isLoading: isOrgProjectsLoading, + isFetched: isOrgProjectsFetched, error: orgProjectsError, - refetch: refetchOrgProjects + refetch: refetchOrgProjects } = useQuery( FrontierServiceQueries.listOrganizationProjects, - create(ListOrganizationProjectsRequestSchema, { + create(ListOrganizationProjectsRequestSchema, { id: organization?.id || '', - withMemberCount + withMemberCount }), { enabled: !!organization?.id && allProjects } ); // Query for current user projects - const { - data: userProjectsData, - isLoading: isUserProjectsLoading, + const { + data: userProjectsData, + isLoading: isUserProjectsLoading, + isFetched: isUserProjectsFetched, error: userProjectsError, - refetch: refetchUserProjects + refetch: refetchUserProjects } = useQuery( FrontierServiceQueries.listProjectsByCurrentUser, - create(ListProjectsByCurrentUserRequestSchema, { + create(ListProjectsByCurrentUserRequestSchema, { orgId: organization?.id || '', withPermissions: ['update', 'delete'], nonInherited: true, - withMemberCount + withMemberCount }), { enabled: !!organization?.id && !allProjects } ); @@ -89,6 +92,7 @@ export const useOrganizationProjects = ({ return { isFetching: isLoading, + isFetched: isUserProjectsFetched || isOrgProjectsFetched, projects: projects, userAccessOnProject, refetch: refetch, diff --git a/web/sdk/react/hooks/useOrganizationTeams.ts b/web/sdk/react/hooks/useOrganizationTeams.ts index ae6e04097..c444996fc 100644 --- a/web/sdk/react/hooks/useOrganizationTeams.ts +++ b/web/sdk/react/hooks/useOrganizationTeams.ts @@ -16,6 +16,7 @@ export const useOrganizationTeams = ({ withMemberCount = false }: useOrganizationTeamsProps): { isFetching: boolean; + isFetched: boolean; teams: Group[]; userAccessOnTeam: Record; refetch: () => void; @@ -27,14 +28,14 @@ export const useOrganizationTeams = ({ const { activeOrganization: organization } = useFrontier(); // Organization teams query - const { data: orgTeamsData, isLoading: isOrgTeamsLoading, error: orgTeamsError, refetch: refetchOrgTeams } = useQuery( + const { data: orgTeamsData, isLoading: isOrgTeamsLoading, isFetched: isOrgTeamsFetched, error: orgTeamsError, refetch: refetchOrgTeams } = useQuery( FrontierServiceQueries.listOrganizationGroups, create(ListOrganizationGroupsRequestSchema, { orgId: organization?.id || '', withMemberCount }), { enabled: !!organization?.id && showOrgTeams } ); - // User teams query - const { data: userTeamsData, isLoading: isUserTeamsLoading, error: userTeamsError, refetch: refetchUserTeams } = useQuery( + // User teams query + const { data: userTeamsData, isLoading: isUserTeamsLoading, isFetched: isUserTeamsFetched, error: userTeamsError, refetch: refetchUserTeams } = useQuery( FrontierServiceQueries.listCurrentUserGroups, create(ListCurrentUserGroupsRequestSchema, { orgId: organization?.id || '', withPermissions, withMemberCount }), { enabled: !!organization?.id && !showOrgTeams } @@ -75,6 +76,7 @@ export const useOrganizationTeams = ({ return { isFetching: isTeamsLoading, + isFetched: isUserTeamsFetched || isOrgTeamsFetched, teams: teams, userAccessOnTeam, refetch, diff --git a/web/sdk/react/views-new/billing/billing-view.tsx b/web/sdk/react/views-new/billing/billing-view.tsx index 0e464b149..83826f606 100644 --- a/web/sdk/react/views-new/billing/billing-view.tsx +++ b/web/sdk/react/views-new/billing/billing-view.tsx @@ -2,8 +2,8 @@ import { useCallback, useEffect, useMemo } from 'react'; import qs from 'query-string'; -import { Flex, Dialog, EmptyState, toastManager } from '@raystack/apsara-v1'; -import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; +import { Flex, Dialog, EmptyState, Image, toastManager } from '@raystack/apsara-v1'; +import exclamationTriangleIcon from '../../assets/exclamation-triangle.svg'; import { CreateCheckoutRequestSchema, ListInvoicesRequestSchema, @@ -162,7 +162,15 @@ export function BillingView({ onNavigateToPlans }: BillingViewProps) { {hasNoAccess ? ( } + variant="empty2" + icon={ + + } heading="Restricted Access" subHeading="Admin access required, please reach out to your admin to view billing." /> diff --git a/web/sdk/react/views-new/members/components/member-columns.tsx b/web/sdk/react/views-new/members/components/member-columns.tsx index 6a1265128..fcc45a5e6 100644 --- a/web/sdk/react/views-new/members/components/member-columns.tsx +++ b/web/sdk/react/views-new/members/components/member-columns.tsx @@ -1,6 +1,6 @@ 'use client'; -import { DotsVerticalIcon } from '@radix-ui/react-icons'; +import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import { Flex, Text, @@ -141,13 +141,13 @@ export const getColumns = ({ }} render={ } > - + ); diff --git a/web/sdk/react/views-new/members/members-view.module.css b/web/sdk/react/views-new/members/members-view.module.css index 78da80ac9..5e32e17cb 100644 --- a/web/sdk/react/views-new/members/members-view.module.css +++ b/web/sdk/react/views-new/members/members-view.module.css @@ -12,6 +12,10 @@ table-layout: fixed; } +.tableRow > td { + padding: var(--rs-space-3); +} + .menuContent { min-width: 180px; } diff --git a/web/sdk/react/views-new/members/members-view.tsx b/web/sdk/react/views-new/members/members-view.tsx index a5fc80f96..74157b69f 100644 --- a/web/sdk/react/views-new/members/members-view.tsx +++ b/web/sdk/react/views-new/members/members-view.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; -import { ExclamationTriangleIcon, TrashIcon, UpdateIcon } from '@radix-ui/react-icons'; +import { ExclamationTriangleIcon, UpdateIcon } from '@radix-ui/react-icons'; import { Button, Tooltip, @@ -11,8 +11,10 @@ import { EmptyState, DataTable, Dialog, + Image, Menu } from '@raystack/apsara-v1'; +import deleteIcon from '../../assets/delete.svg'; import { useFrontier } from '../../contexts/FrontierContext'; import { useOrganizationMembers } from '../../hooks/useOrganizationMembers'; import { usePermissions } from '../../hooks/usePermissions'; @@ -189,7 +191,8 @@ export function MembersView({ showTeamField = true }: MembersViewProps) { } classNames={{ root: styles.tableRoot, - table: styles.table + table: styles.table, + row: styles.tableRow }} /> @@ -218,7 +221,14 @@ export function MembersView({ showTeamField = true }: MembersViewProps) { ))} {payload?.canRemove && ( } + leadingIcon={ + Remove + } onClick={() => removeMemberDialogHandle.openWithPayload({ memberId: payload.memberId, @@ -226,6 +236,9 @@ export function MembersView({ showTeamField = true }: MembersViewProps) { }) } data-test-id="remove-member-dropdown-item" + style={{ + color: 'var(--rs-color-foreground-danger-primary)' + }} > Remove diff --git a/web/sdk/react/views-new/pat/pat-view.tsx b/web/sdk/react/views-new/pat/pat-view.tsx index 6a4fb2460..5df99a715 100644 --- a/web/sdk/react/views-new/pat/pat-view.tsx +++ b/web/sdk/react/views-new/pat/pat-view.tsx @@ -9,8 +9,10 @@ import { Dialog, EmptyState, Flex, + Image, Skeleton } from '@raystack/apsara-v1'; +import keyIcon from '../../assets/key.svg'; import type { DataTableQuery, DataTableSort } from '@raystack/apsara-v1'; import { useDebouncedState } from '@raystack/apsara-v1/hooks'; import { useInfiniteQuery } from '@connectrpc/connect-query'; @@ -166,13 +168,22 @@ export function PatsView({ onPATClick }: PatsViewProps = {}) { ) : hasNoPats ? ( } + variant="empty2" + icon={ + + } heading="No Personal Access Token Found" subHeading={`Create a new to use the Keys of ${t.appName()} platform`} primaryAction={ + + + ); +} + +export function AddMemberMenu({ + projectId, + canUpdateProject, + members, + refetch +}: AddMemberMenuProps) { if (!canUpdateProject) { return ( @@ -183,87 +288,12 @@ export function AddMemberMenu({ > Add a member - -
- {isLoading - ? - { - Array.from({ length: 6 }, (_, i) => ( - - )) - } - - : showTeam - ? teams.map(team => ( - - } - onClick={() => addTeam(team.id || '')} - data-test-id={`frontier-sdk-add-team-to-project-item-${team.id}`} - > - {team.title || team.name} - - )) - : invitableUsers.map(user => ( - - } - onClick={() => addMember(user.id || '')} - data-test-id={`frontier-sdk-add-user-to-project-item-${user.id}`} - > - {user.title || user.email} - - ))} - - {!isLoading && - (showTeam ? !teams.length : !invitableUsers.length) && ( - - {showTeam ? 'No teams found' : 'No users found'} - - )} -
- - - - -
+ ); } diff --git a/web/sdk/react/views-new/projects/components/edit-project-dialog.module.css b/web/sdk/react/views-new/projects/components/edit-project-dialog.module.css deleted file mode 100644 index 0aa54aa46..000000000 --- a/web/sdk/react/views-new/projects/components/edit-project-dialog.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.radioOptions { - display: flex; - gap: var(--rs-space-10); -} diff --git a/web/sdk/react/views-new/projects/components/edit-project-dialog.tsx b/web/sdk/react/views-new/projects/components/edit-project-dialog.tsx index 4c23ce005..76e999260 100644 --- a/web/sdk/react/views-new/projects/components/edit-project-dialog.tsx +++ b/web/sdk/react/views-new/projects/components/edit-project-dialog.tsx @@ -4,11 +4,9 @@ import { useEffect } from 'react'; import { Button, Flex, - Text, Dialog, Field, - Input, - Radio + Input } from '@raystack/apsara-v1'; import { toastManager } from '@raystack/apsara-v1'; import * as yup from 'yup'; @@ -20,13 +18,11 @@ import { UpdateProjectRequestSchema } from '@raystack/proton/frontier'; import { create } from '@bufbuild/protobuf'; -import styles from './edit-project-dialog.module.css'; import { handleConnectError } from '~/utils/error'; const editProjectSchema = yup .object({ - title: yup.string().required('Project name is required'), - privacy: yup.string().oneOf(['private', 'public']).required() + title: yup.string().required('Project name is required') }) .required(); @@ -76,28 +72,22 @@ function EditProjectForm({ payload, handle, refetch }: EditProjectFormProps) { const { reset, handleSubmit, - setValue, - watch, formState: { errors, isSubmitting, isDirty }, register } = useForm({ resolver: yupResolver(editProjectSchema), defaultValues: { - title: payload.title, - privacy: 'private' as const + title: payload.title } }); - const privacy = watch('privacy'); - const { mutateAsync: updateProject } = useMutation( FrontierServiceQueries.updateProject ); useEffect(() => { reset({ - title: payload.title, - privacy: 'private' + title: payload.title }); }, [payload.projectId, payload.title, reset]); @@ -145,34 +135,6 @@ function EditProjectForm({ payload, handle, refetch }: EditProjectFormProps) { placeholder="Enter project name" /> - - - Project privacy - - - setValue('privacy', val as 'private' | 'public', { - shouldDirty: true - }) - } - > -
- - - - Private - - - - - - Public - - -
-
-
diff --git a/web/sdk/react/views-new/projects/components/member-columns.module.css b/web/sdk/react/views-new/projects/components/member-columns.module.css new file mode 100644 index 000000000..72da9c120 --- /dev/null +++ b/web/sdk/react/views-new/projects/components/member-columns.module.css @@ -0,0 +1,7 @@ +.actionsCell { + visibility: hidden; +} + +tr:hover .actionsCell { + visibility: visible; +} diff --git a/web/sdk/react/views-new/projects/components/member-columns.tsx b/web/sdk/react/views-new/projects/components/member-columns.tsx index 738c04276..3e0af7b90 100644 --- a/web/sdk/react/views-new/projects/components/member-columns.tsx +++ b/web/sdk/react/views-new/projects/components/member-columns.tsx @@ -1,6 +1,6 @@ 'use client'; -import { DotsVerticalIcon, TrashIcon, UpdateIcon } from '@radix-ui/react-icons'; +import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import { Flex, Text, @@ -13,6 +13,7 @@ import { import type { User, Group, Role } from '@raystack/proton/frontier'; import { getInitials } from '~/utils'; import teamIcon from '~/react/assets/users.svg'; +import styles from './member-columns.module.css'; export type MemberRow = (Group & { isTeam: true }) | (User & { isTeam?: false }); @@ -52,7 +53,7 @@ export function getColumns({ const label = member.isTeam ? member.title : member.title; const subLabel = member.isTeam ? member.name : member.email; return ( - + !currentRoleIds.has(r.id)); return ( - + } > - + ); diff --git a/web/sdk/react/views-new/projects/components/project-columns.module.css b/web/sdk/react/views-new/projects/components/project-columns.module.css new file mode 100644 index 000000000..72da9c120 --- /dev/null +++ b/web/sdk/react/views-new/projects/components/project-columns.module.css @@ -0,0 +1,7 @@ +.actionsCell { + visibility: hidden; +} + +tr:hover .actionsCell { + visibility: visible; +} diff --git a/web/sdk/react/views-new/projects/components/project-columns.tsx b/web/sdk/react/views-new/projects/components/project-columns.tsx index 9b514be3b..f98548f84 100644 --- a/web/sdk/react/views-new/projects/components/project-columns.tsx +++ b/web/sdk/react/views-new/projects/components/project-columns.tsx @@ -1,6 +1,6 @@ 'use client'; -import { DotsVerticalIcon, Pencil1Icon, TrashIcon } from '@radix-ui/react-icons'; +import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import { Flex, Text, @@ -10,6 +10,7 @@ import { } from '@raystack/apsara-v1'; import type { Project } from '@raystack/proton/frontier'; import { MembersCell } from './members-cell'; +import styles from './project-columns.module.css'; export interface ProjectMenuPayload { projectId: string; @@ -41,19 +42,6 @@ export const getColumns = ({ ); } }, - { - header: 'Privacy', - accessorKey: 'metadata', - enableSorting: false, - - cell: () => { - return ( - - Private - - ); - } - }, { header: 'Members', accessorKey: 'membersCount', @@ -84,7 +72,7 @@ export const getColumns = ({ if (!canUpdate && !canDelete) return null; return ( - + } > - + ); diff --git a/web/sdk/react/views-new/projects/project-details-view.module.css b/web/sdk/react/views-new/projects/project-details-view.module.css index 2674282bc..a9a2e35eb 100644 --- a/web/sdk/react/views-new/projects/project-details-view.module.css +++ b/web/sdk/react/views-new/projects/project-details-view.module.css @@ -12,6 +12,10 @@ table-layout: fixed; } +.tableRow > td { + padding: var(--rs-space-3); +} + .menuContent { min-width: 160px; } diff --git a/web/sdk/react/views-new/projects/project-details-view.tsx b/web/sdk/react/views-new/projects/project-details-view.tsx index f06cacf6a..b0e77dbf3 100644 --- a/web/sdk/react/views-new/projects/project-details-view.tsx +++ b/web/sdk/react/views-new/projects/project-details-view.tsx @@ -414,7 +414,8 @@ export function ProjectDetailsView({ } classNames={{ root: styles.tableRoot, - table: styles.table + table: styles.table, + row: styles.tableRow }} /> diff --git a/web/sdk/react/views-new/projects/projects-view.module.css b/web/sdk/react/views-new/projects/projects-view.module.css index ef5ab7df4..9d3c0abbc 100644 --- a/web/sdk/react/views-new/projects/projects-view.module.css +++ b/web/sdk/react/views-new/projects/projects-view.module.css @@ -9,3 +9,9 @@ .menuContent { min-width: 6; } + +.projectsFilter { + width: auto; + min-width: 140px; + box-shadow: none; +} diff --git a/web/sdk/react/views-new/projects/projects-view.tsx b/web/sdk/react/views-new/projects/projects-view.tsx index 291c96d4e..4c64c4244 100644 --- a/web/sdk/react/views-new/projects/projects-view.tsx +++ b/web/sdk/react/views-new/projects/projects-view.tsx @@ -1,10 +1,10 @@ 'use client'; -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { - ArchiveIcon, ExclamationTriangleIcon, - Pencil1Icon + Pencil1Icon, + PlusIcon } from '@radix-ui/react-icons'; import { Button, @@ -16,9 +16,11 @@ import { Dialog, AlertDialog, Image, - Menu + Menu, + Select } from '@raystack/apsara-v1'; import deleteIcon from '../../assets/delete.svg'; +import inboxStackIcon from '../../assets/inbox-stack.svg'; import { toastManager } from '@raystack/apsara-v1'; import { useFrontier } from '../../contexts/FrontierContext'; import { useOrganizationProjects } from '../../hooks/useOrganizationProjects'; @@ -31,9 +33,15 @@ import { getColumns, type ProjectMenuPayload } from './components/project-column import { AddProjectDialog } from './components/add-project-dialog'; import { EditProjectDialog, type EditProjectPayload } from './components/edit-project-dialog'; import { DeleteProjectDialog, type DeleteProjectPayload } from './components/delete-project-dialog'; +import { AddMemberMenuContent } from './components/add-member-menu'; import styles from './projects-view.module.css'; import { useTerminology } from '~/react/hooks/useTerminology'; +const projectsFilterOptions = [ + { value: 'my-projects', label: 'My Projects' }, + { value: 'all-projects', label: 'All Projects' } +]; + const projectMenuHandle = Menu.createHandle(); const addProjectDialogHandle = Dialog.createHandle(); const editProjectDialogHandle = Dialog.createHandle(); @@ -50,13 +58,17 @@ export function ProjectsView({ description, onProjectClick }: ProjectsViewProps) { + const [showOrgProjects, setShowOrgProjects] = useState(false); + const { isFetching: isProjectsLoading, + isFetched: isProjectsFetched, projects, userAccessOnProject, refetch, error: projectsError } = useOrganizationProjects({ + allProjects: showOrgProjects, withMemberCount: true }); @@ -83,15 +95,23 @@ export function ProjectsView({ !!organization?.id ); - const { canCreateProject } = useMemo(() => { + const { canCreateProject, canListOrgProjects } = useMemo(() => { return { canCreateProject: shouldShowComponent( permissions, `${PERMISSIONS.ProjectCreatePermission}::${resource}` + ), + canListOrgProjects: shouldShowComponent( + permissions, + `${PERMISSIONS.UpdatePermission}::${resource}` ) }; }, [permissions, resource]); + const onFilterChange = useCallback((value: string) => { + setShowOrgProjects(value === 'all-projects'); + }, []); + useEffect(() => { if (projectsError) { toastManager.add({ @@ -106,6 +126,9 @@ export function ProjectsView({ }, [projectsError]); const isLoading = !organization?.id || isPermissionsFetching || isProjectsLoading; + const showInitialSkeleton = isLoading && !isProjectsFetched; + const filterValue = showOrgProjects ? 'all-projects' : 'my-projects'; + const hasNoProjects = !isLoading && (projects?.length ?? 0) === 0; const columns = useMemo( () => @@ -116,39 +139,39 @@ export function ProjectsView({ [userAccessOnProject] ); - if (!isLoading && (projects?.length ?? 0) === 0) { + if (hasNoProjects) { return ( } + icon={ + + } heading={t.project()} subHeading="A project is a structured initiative undertaken to achieve a specific outcome. It operates within a defined scope, objectives, and resources, following a process of planning, execution, monitoring, and completion." secondaryAction={ - - } + canCreateProject ? ( + - - {!canCreateProject && ( - {AuthTooltipMessage} - )} - + Create new {t.project({ case: 'lower' })} + + ) : null } /> - + {canCreateProject && ( + + )} ); } @@ -168,17 +191,38 @@ export function ProjectsView({ - {isLoading ? ( + {showInitialSkeleton ? ( ) : ( - + <> + + {canListOrgProjects && ( + + )} + )} - {isLoading ? ( + {showInitialSkeleton ? ( ) : ( @@ -238,6 +282,23 @@ export function ProjectsView({ Edit )} + {payload?.canUpdate && ( + + } + data-test-id="add-project-member-dropdown-item" + > + Add a member + + + + )} {payload?.canDelete && ( } diff --git a/web/sdk/react/views-new/service-accounts/components/service-account-columns.tsx b/web/sdk/react/views-new/service-accounts/components/service-account-columns.tsx index b889465d1..a1e50b86d 100644 --- a/web/sdk/react/views-new/service-accounts/components/service-account-columns.tsx +++ b/web/sdk/react/views-new/service-accounts/components/service-account-columns.tsx @@ -1,6 +1,6 @@ 'use client'; -import { DotsVerticalIcon } from '@radix-ui/react-icons'; +import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import { Flex, Text, @@ -81,14 +81,14 @@ export const getColumns = ({ }} render={ } > - + ); diff --git a/web/sdk/react/views-new/service-accounts/service-accounts-view.tsx b/web/sdk/react/views-new/service-accounts/service-accounts-view.tsx index 99f7f0b63..df6907951 100644 --- a/web/sdk/react/views-new/service-accounts/service-accounts-view.tsx +++ b/web/sdk/react/views-new/service-accounts/service-accounts-view.tsx @@ -16,6 +16,7 @@ import { } from '@raystack/apsara-v1'; import deleteIcon from '~/react/assets/delete.svg'; import keyIcon from '~/react/assets/key.svg'; +import exclamationTriangleIcon from '~/react/assets/exclamation-triangle.svg'; import { useQuery } from '@connectrpc/connect-query'; import { create } from '@bufbuild/protobuf'; import { @@ -146,19 +147,36 @@ export function ServiceAccountsView({ {hasNoAccess ? ( } + variant="empty2" + icon={ + + } heading="Restricted Access" subHeading="Admin access required, please reach out to your admin incase you want to generate a key." /> ) : hasNoServiceAccounts ? ( } + variant="empty2" + icon={ + + } heading="No Service Account Found" subHeading={`Create a new account to use the APIs of ${t.appName()} platform`} primaryAction={ + ) : null + } + /> + {canCreateGroup && ( + + )} + + ); + } + return ( - {isLoading ? ( + {showInitialSkeleton ? ( ) : ( <> @@ -159,11 +207,13 @@ export function TeamsView({ placeholder="Search by title" size="large" width={360} + disabled={isLoading} /> {canListOrgGroups && (