feat: introducing i18n support to admin dashboard (#3051)

This commit is contained in:
Joel Jacob Stephen
2023-06-16 07:17:00 +03:00
committed by GitHub
parent b07243f131
commit 331d482b22
58 changed files with 905 additions and 184 deletions

View File

@@ -1,38 +1,40 @@
<template>
<div class="flex flex-col">
<h1 class="text-lg font-bold text-secondaryDark">Dashboard</h1>
<h1 class="text-lg font-bold text-secondaryDark">
{{ t('metrics.dashboard') }}
</h1>
<div v-if="fetching" class="flex justify-center py-6">
<HoppSmartSpinner />
</div>
<div v-else-if="error || !metrics">
<p class="text-xl">No Metrics Found..</p>
<p class="text-xl">{{ t('metrics.no_metrics') }}</p>
</div>
<div v-else>
<div class="py-10 grid lg:grid-cols-2 gap-6">
<DashboardMetricsCard
:count="metrics.usersCount"
label="Total Users"
:label="t('metrics.total_users')"
:icon="UserIcon"
color="text-green-400"
/>
<DashboardMetricsCard
:count="metrics.teamsCount"
label="Total Teams"
:label="t('metrics.total_teams')"
:icon="UsersIcon"
color="text-pink-400"
/>
<DashboardMetricsCard
:count="metrics.teamRequestsCount"
label="Total Requests"
:label="t('metrics.total_requests')"
:icon="LineChartIcon"
color="text-cyan-400"
/>
<DashboardMetricsCard
:count="metrics.teamCollectionsCount"
label="Total Collections"
:label="t('metrics.total_collections')"
:icon="FolderTreeIcon"
color="text-orange-400"
/>
@@ -49,6 +51,9 @@ import UserIcon from '~icons/lucide/user';
import UsersIcon from '~icons/lucide/users';
import LineChartIcon from '~icons/lucide/line-chart';
import FolderTreeIcon from '~icons/lucide/folder-tree';
import { useI18n } from '../composables/i18n';
const t = useI18n();
// Get Metrics Data
const { fetching, error, data } = useQuery({ query: MetricsDocument });

View File

@@ -25,7 +25,7 @@
<div class="py-8">
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
<HoppSmartTab :id="'details'" label="Details">
<HoppSmartTab :id="'details'" :label="t('teams.details')">
<TeamsDetails
:team="team"
:teamName="teamName"
@@ -35,17 +35,17 @@
class="py-8 px-4"
/>
</HoppSmartTab>
<HoppSmartTab :id="'members'" label="Members">
<HoppSmartTab :id="'members'" :label="t('teams.team_members')">
<TeamsMembers @update-team="updateTeam()" class="py-8 px-4" />
</HoppSmartTab>
<HoppSmartTab :id="'invites'" label="Invites">
<HoppSmartTab :id="'invites'" :label="t('teams.invites')">
<TeamsPendingInvites :editingTeamID="team.id" class="py-8 px-4" />
</HoppSmartTab>
</HoppSmartTabs>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="`Confirm Deletion of ${team.name} team?`"
:title="t('teams.confirm_team_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteTeamMutation(deleteTeamUID)"
/>
@@ -58,7 +58,7 @@
import { useClientHandle, useMutation } from '@urql/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from '../../composables/toast';
import { useToast } from '~/composables/toast';
import {
RemoveTeamDocument,
RenameTeamDocument,
@@ -67,6 +67,9 @@ import {
TeamInfoQuery,
} from '../../helpers/backend/graphql';
import { HoppSmartTabs } from '@hoppscotch/ui';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -77,11 +80,11 @@ const selectedOptionTab = ref<OptionTabs>('details');
const currentTabName = computed(() => {
switch (selectedOptionTab.value) {
case 'details':
return 'Team details';
return t('teams.team_details');
case 'members':
return 'Team members';
return t('teams.team_members_tab');
case 'invites':
return 'Pending invites';
return t('teams.pending_invites');
default:
return '';
}
@@ -100,7 +103,7 @@ const getTeamInfo = async () => {
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
.toPromise();
if (result.error) {
return toast.error('Unable to load team info..');
return toast.error(`${t('team.load_info_error')}`);
}
if (result.data?.admin.teamInfo) {
team.value = result.data.admin.teamInfo;
@@ -127,12 +130,12 @@ const renameTeamName = async (teamName: string) => {
const variables = { uid: team.value.id, name: teamName };
await teamRename.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Failed to rename team!!');
toast.error(`${t('state.rename_team_failure')}`);
} else {
showRenameInput.value = false;
if (team.value) {
team.value.name = teamName;
toast.success('Team renamed successfully!!');
toast.success(`${t('state.rename_team_success')}`);
}
}
});
@@ -152,15 +155,15 @@ const deleteTeam = (id: string) => {
const deleteTeamMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error('Team deletion failed!!');
toast.error(`${t('state.delete_team_failure')}`);
return;
}
const variables = { uid: id };
await teamDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Team deletion failed!!');
toast.error(`${t('state.delete_team_failure')}`);
} else {
toast.success('Team deleted successfully!!');
toast.success(`${t('state.delete_team_success')}`);
}
});
confirmDeletion.value = false;

View File

@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col">
<h1 class="text-lg font-bold text-secondaryDark">Teams</h1>
<h1 class="text-lg font-bold text-secondaryDark">{{ t('teams.teams') }}</h1>
<div class="flex flex-col">
<div class="flex py-10">
<HoppButtonPrimary
:icon="IconAddUsers"
label="Create team"
:label="t('teams.create_team')"
@click="showCreateTeamModal = true"
/>
</div>
@@ -19,7 +19,7 @@
<HoppSmartSpinner />
</div>
<div v-else-if="error">Unable to Load Teams List..</div>
<div v-else-if="error">{{ t('teams.load_list_error') }}</div>
<TeamsTable
v-else
@@ -34,7 +34,7 @@
class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark"
@click="fetchNextTeams"
>
<span>Show more </span>
<span>{{ t('teams.show_more') }}</span>
<icon-lucide-chevron-down class="ml-2 text-lg" />
</div>
</div>
@@ -50,7 +50,7 @@
/>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="`Confirm Deletion of the team?`"
:title="t('teams.confirm_team_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteTeamMutation(deleteTeamID)"
/>
@@ -65,11 +65,14 @@ import {
TeamListDocument,
UsersListDocument,
} from '../../helpers/backend/graphql';
import { usePagedQuery } from '../../composables/usePagedQuery';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { ref, watch, computed } from 'vue';
import { useMutation, useQuery } from '@urql/vue';
import { useToast } from '../../composables/toast';
import { useToast } from '~/composables/toast';
import IconAddUsers from '~icons/lucide/plus';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
// Get Users List
@@ -110,11 +113,11 @@ const createTeamLoading = ref(false);
const createTeam = async (newTeamName: string, ownerEmail: string) => {
if (newTeamName.length < 6) {
toast.error('Team name should be atleast 6 characters long!!');
toast.error(`${t('state.team_name_long')}`);
return;
}
if (ownerEmail.length == 0) {
toast.error('Please enter email of team owner!!');
toast.error(`${t('state.enter_team_email')}`);
return;
}
createTeamLoading.value = true;
@@ -124,13 +127,13 @@ const createTeam = async (newTeamName: string, ownerEmail: string) => {
await createTeamMutation.executeMutation(variables).then((result) => {
if (result.error) {
if (result.error.toString() == '[GraphQL] user/not_found') {
toast.error('User not found!!');
toast.error(`${t('state.user_not_found')}`);
} else {
toast.error('Failed to create team!!');
toast.error(`${t('state.create_team_failure')}`);
}
createTeamLoading.value = false;
} else {
toast.success('Team created successfully!!');
toast.success(`${t('state.create_team_success')}`);
showCreateTeamModal.value = false;
createTeamLoading.value = false;
refetch();
@@ -164,16 +167,16 @@ const deleteTeam = (id: string) => {
const deleteTeamMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error('Team deletion failed!!');
toast.error(`${t('state.delete_team_failure')}`);
return;
}
const variables = { uid: id };
await teamDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Team deletion failed!!');
toast.error(`${t('state.delete_team_failure')}`);
} else {
teamList.value = teamList.value.filter((team) => team.id !== id);
toast.success('Team deleted successfully!!');
toast.success(`${t('state.delete_team_success')}`);
}
});
confirmDeletion.value = false;

View File

@@ -18,7 +18,7 @@
v-if="user.isAdmin"
class="absolute left-17 bottom-0 text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
>
Admin
{{ t('users.admin') }}
</span>
</div>
@@ -28,12 +28,14 @@
v-if="user.isAdmin"
class="absolute left-15 bottom-0 text-xs font-medium px-3 py-0.5 rounded-full bg-green-900 text-green-300"
>
Admin
{{ t('users.admin') }}
</span>
</div>
<div v-if="user.uid">
<label class="text-secondaryDark" for="username">UID</label>
<label class="text-secondaryDark" for="username">{{
t('users.uid')
}}</label>
<div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
>
@@ -41,18 +43,22 @@
</div>
</div>
<div>
<label class="text-secondaryDark" for="username">Name</label>
<label class="text-secondaryDark" for="username">{{
t('users.name')
}}</label>
<div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
>
<span v-if="user.displayName">
{{ user.displayName }}
</span>
<span v-else> (Unnamed user) </span>
<span v-else> {{ t('users.unnamed') }} </span>
</div>
</div>
<div v-if="user.email">
<label class="text-secondaryDark" for="username">Email</label>
<label class="text-secondaryDark" for="username">{{
t('users.email')
}}</label>
<div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-200 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
>
@@ -60,7 +66,9 @@
</div>
</div>
<div v-if="user.createdOn">
<label class="text-secondaryDark" for="username">Created On</label>
<label class="text-secondaryDark" for="username">{{
t('users.created_on')
}}</label>
<div
class="w-full p-3 mt-2 bg-zinc-800 border-gray-600 rounded-md focus:border-emerald-600 focus:ring focus:ring-opacity-40 focus:ring-emerald-500"
>
@@ -75,7 +83,7 @@
class="mr-4"
filled
outline
label="Make Admin"
:label="t('users.make_admin')"
@click="makeUserAdmin(user.uid)"
/>
</span>
@@ -85,7 +93,7 @@
filled
outline
:icon="IconUserMinus"
label="Remove Admin Privilege"
:label="t('users.remove_admin_privilege')"
@click="makeAdminToUser(user.uid)"
/>
</span>
@@ -94,7 +102,7 @@
class="mr-4 !bg-red-600 !text-gray-300 !hover:text-gray-100"
filled
outline
label="Delete"
:label="t('users.delete')"
:icon="IconTrash"
@click="deleteUser(user.uid)"
/>
@@ -105,26 +113,26 @@
filled
outline
:icon="IconTrash"
label="Delete"
@click="toast.error('Remove admin privilege to delete the user!!')"
:label="t('users.delete')"
@click="toast.error(t('state.remove_admin_to_delete_user'))"
/>
</div>
</div>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="`Confirm deletion of user?`"
:title="t('users.confirm_user_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteUserMutation(deleteUserUID)"
/>
<HoppSmartConfirmModal
:show="confirmUserToAdmin"
:title="`Do you want to make this user into an admin?`"
:title="t('users.confirm_user_to_admin')"
@hide-modal="confirmUserToAdmin = false"
@resolve="makeUserAdminMutation(userToAdminUID)"
/>
<HoppSmartConfirmModal
:show="confirmAdminToUser"
:title="`Do you want to remove admin status from this user?`"
:title="t('users.confirm_admin_to_user')"
@hide-modal="confirmAdminToUser = false"
@resolve="makeAdminToUserMutation(adminToUserUID)"
/>
@@ -143,9 +151,12 @@ import {
import { useClientHandle } from '@urql/vue';
import { format } from 'date-fns';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from '../../composables/toast';
import { useToast } from '~/composables/toast';
import IconTrash from '~icons/lucide/trash';
import IconUserMinus from '~icons/lucide/user-minus';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -166,7 +177,7 @@ onMounted(async () => {
.toPromise();
if (result.error) {
toast.error('Unable to load user info..');
toast.error(`${t('users.load_info_error')}`);
}
user.value = result.data?.admin.userInfo ?? {};
fetching.value = false;
@@ -186,15 +197,15 @@ const deleteUser = (id: string) => {
const deleteUserMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error('User deletion failed!!');
toast.error(`${t('state.delete_user_failure')}`);
return;
}
const variables = { uid: id };
await userDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('User deletion failed!!');
toast.error(`${t('state.delete_user_failure')}`);
} else {
toast.success('User deleted successfully!!');
toast.success(`${t('state.delete_user_success')}`);
}
});
confirmDeletion.value = false;
@@ -215,16 +226,16 @@ const makeUserAdmin = (id: string) => {
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error('User deletion failed!!');
toast.error(`${t('state.admin_failure')}`);
return;
}
const variables = { uid: id };
await userToAdmin.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Failed to make user an admin!!');
toast.error(`${t('state.admin_failure')}`);
} else {
user.value.isAdmin = true;
toast.success('User is now an admin!!');
toast.success(`${t('state.admin_success')}`);
}
});
confirmUserToAdmin.value = false;
@@ -244,16 +255,16 @@ const makeAdminToUser = (id: string) => {
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error('Failed to remove admin status!!');
toast.error(`${t('state.remove_admin_failure')}`);
return;
}
const variables = { uid: id };
await adminToUser.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Failed to remove admin status!!');
toast.error(`${t('state.remove_admin_failure')}`);
} else {
user.value.isAdmin = false;
toast.success('Admin status removed!!');
toast.error(`${t('state.remove_admin_success')}`);
}
});
confirmAdminToUser.value = false;

View File

@@ -2,10 +2,12 @@
<div class="flex flex-col">
<!-- Table View for All Users -->
<div class="flex flex-col">
<h1 class="text-lg font-bold text-secondaryDark">Users</h1>
<h1 class="text-lg font-bold text-secondaryDark">
{{ t('users.users') }}
</h1>
<div class="flex items-center space-x-4 py-10">
<HoppButtonPrimary
label="Invite a user"
:label="t('users.invite_user')"
@click="showInviteUserModal = true"
:icon="IconAddUser"
/>
@@ -14,7 +16,7 @@
<HoppButtonSecondary
outline
filled
label="Invited users"
:label="t('users.invited_users')"
:to="'/users/invited'"
/>
</div>
@@ -27,7 +29,7 @@
<HoppSmartSpinner />
</div>
<div v-else-if="error">Unable to Load Users List..</div>
<div v-else-if="error">{{ t('users.load_list_error') }}</div>
<UsersTable
v-else-if="usersList.length >= 1"
@@ -40,14 +42,14 @@
@deleteUser="deleteUser"
/>
<div v-else class="flex justify-center">No Users Found</div>
<div v-else class="flex justify-center">{{ t('users.no_users') }}</div>
<div
v-if="hasNextPage && usersList.length >= usersPerPage"
class="flex justify-center my-5 px-3 py-2 cursor-pointer font-semibold rounded-3xl bg-dividerDark hover:bg-divider transition mx-auto w-38 text-secondaryDark"
@click="fetchNextUsers"
>
<span>Show more </span>
<span>{{ t('users.show_more') }}</span>
<icon-lucide-chevron-down class="ml-2 text-lg" />
</div>
</div>
@@ -60,19 +62,19 @@
/>
<HoppSmartConfirmModal
:show="confirmDeletion"
:title="`Confirm user deletion?`"
:title="t('users.confirm_user_deletion')"
@hide-modal="confirmDeletion = false"
@resolve="deleteUserMutation(deleteUserUID)"
/>
<HoppSmartConfirmModal
:show="confirmUserToAdmin"
:title="`Do you want to make this user into an admin?`"
:title="t('users.confirm_user_to_admin')"
@hide-modal="confirmUserToAdmin = false"
@resolve="makeUserAdminMutation(userToAdminUID)"
/>
<HoppSmartConfirmModal
:show="confirmAdminToUser"
:title="`Do you want to remove admin status from this user?`"
:title="t('users.confirm_admin_to_user')"
@hide-modal="confirmAdminToUser = false"
@resolve="makeAdminToUserMutation(adminToUserUID)"
/>
@@ -89,11 +91,14 @@ import {
RemoveUserAsAdminDocument,
UsersListDocument,
} from '../../helpers/backend/graphql';
import { usePagedQuery } from '../../composables/usePagedQuery';
import { usePagedQuery } from '~/composables/usePagedQuery';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from '../../composables/toast';
import { useToast } from '~/composables/toast';
import { HoppButtonSecondary } from '@hoppscotch/ui';
import IconAddUser from '~icons/lucide/user-plus';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const toast = useToast();
@@ -119,15 +124,15 @@ const showInviteUserModal = ref(false);
const sendInvite = async (email: string) => {
if (!email.trim()) {
toast.error('Please enter a valid email address');
toast.error(`${t('state.invalid_email')}`);
return;
}
const variables = { inviteeEmail: email.trim() };
await sendInvitation.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Failed to send invitation');
toast.error(`${t('state.email_failure')}`);
} else {
toast.success('Email invitation sent successfully');
toast.success(`${t('state.email_success')}`);
showInviteUserModal.value = false;
}
});
@@ -153,15 +158,15 @@ const deleteUserUID = ref<string | null>(null);
const deleteUserMutation = async (id: string | null) => {
if (!id) {
confirmDeletion.value = false;
toast.error('User deletion failed!!');
toast.error(`${t('state.delete_user_failure')}`);
return;
}
const variables = { uid: id };
await userDeletion.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('User deletion failed!!');
toast.error(`${t('state.delete_user_failure')}`);
} else {
toast.success('User deleted successfully!!');
toast.success(`${t('state.delete_user_success')}`);
usersList.value = usersList.value.filter((user) => user.uid !== id);
}
});
@@ -182,15 +187,15 @@ const makeUserAdmin = (id: string) => {
const makeUserAdminMutation = async (id: string | null) => {
if (!id) {
confirmUserToAdmin.value = false;
toast.error('Failed to make user an admin!!');
toast.error(`${t('state.admin_failure')}`);
return;
}
const variables = { uid: id };
await userToAdmin.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Failed to make user an admin!!');
toast.error(`${t('state.admin_failure')}`);
} else {
toast.success('User is now an admin!!');
toast.success(`${t('state.admin_success')}`);
usersList.value = usersList.value.map((user) => {
if (user.uid === id) {
user.isAdmin = true;
@@ -221,15 +226,15 @@ const deleteUser = (id: string) => {
const makeAdminToUserMutation = async (id: string | null) => {
if (!id) {
confirmAdminToUser.value = false;
toast.error('Failed to remove admin status!!');
toast.error(`${t('state.remove_admin_failure')}`);
return;
}
const variables = { uid: id };
await adminToUser.executeMutation(variables).then((result) => {
if (result.error) {
toast.error('Failed to remove admin status!!');
toast.error(`${t('state.remove_admin_failure')}`);
} else {
toast.success('Admin status removed!!');
toast.success(`${t('state.remove_admin_success')}`);
usersList.value = usersList.value.map((user) => {
if (user.uid === id) {
user.isAdmin = false;

View File

@@ -6,7 +6,9 @@
</button>
</div>
<h3 class="text-lg font-bold text-accentContrast py-6">Invited Users</h3>
<h3 class="text-lg font-bold text-accentContrast py-6">
{{ t('users.invited_users') }}
</h3>
<div class="flex flex-col">
<div class="py-2 overflow-x-auto">
@@ -15,7 +17,7 @@
<HoppSmartSpinner />
</div>
<div v-else-if="error || invitedUsers === undefined">
<p class="text-xl">No invited users found..</p>
<p class="text-xl">{{ t('users.no_invite') }}</p>
</div>
<table v-else class="w-full text-left">
@@ -23,10 +25,10 @@
<tr
class="text-secondary border-b border-dividerDark text-sm text-left"
>
<th class="px-3 pb-3">Admin ID</th>
<th class="px-3 pb-3">Admin Email</th>
<th class="px-3 pb-3">Invitee Email</th>
<th class="px-3 pb-3">Invited On</th>
<th class="px-3 pb-3">{{ t('users.admin_id') }}</th>
<th class="px-3 pb-3">{{ t('users.admin_email') }}</th>
<th class="px-3 pb-3">{{ t('users.invitee_email') }}</th>
<th class="px-3 pb-3">{{ t('users.invited_on') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-divider">
@@ -34,7 +36,7 @@
v-if="invitedUsers.length === 0"
class="text-secondaryDark py-4"
>
<div class="py-6 px-3">No invited users found..</div>
<div class="py-6 px-3">{{ t('users.no_invite') }}</div>
</tr>
<tr
v-else
@@ -85,6 +87,9 @@ import { InvitedUsersDocument } from '../../helpers/backend/graphql';
import { format } from 'date-fns';
import { HoppSmartSpinner } from '@hoppscotch/ui';
import { useRouter } from 'vue-router';
import { useI18n } from '~/composables/i18n';
const t = useI18n();
const router = useRouter();