Skip to content

Commit

Permalink
Add skills analytics chart to skills page
Browse files Browse the repository at this point in the history
  • Loading branch information
alessandromontividiu03 authored and kaiomagalhaes committed Nov 5, 2024
1 parent ddd44c5 commit e005b42
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 55 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"eslint": "8.48.0",
"eslint-config-next": "^14.0.2",
"eslint-plugin-typescript-sort-keys": "^3.1.0",
"lodash.debounce": "^4.0.8",
"material-icons": "^1.13.11",
"next": "^14.0.2",
"next-auth": "^4.24.5",
Expand Down
8 changes: 8 additions & 0 deletions src/app/_domain/interfaces/Skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ export type ApiUserSkill = {
user_id?: number;
years_of_experience: number;
};

export type SkillAnalytics = {
level: {
count: number;
name: string;
}[],
name: string;
};
13 changes: 11 additions & 2 deletions src/app/_presenters/data/skills/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Skill } from "@/app/_domain/interfaces/Skill";
import { Skill, SkillAnalytics } from "@/app/_domain/interfaces/Skill";

import { backstageApiClient } from "../auth/backstageApiAxios";

Expand All @@ -10,4 +10,13 @@ export const getSkills = async (): Promise<Skill[] | null> => {
export const createSkill = async (skill: Skill): Promise<Skill | null> => {
const { data } = await backstageApiClient.post("/skills", skill);
return data;
}
};

export const getSkillsAnalytics = async (search: string): Promise<SkillAnalytics[] | null> => {
const { data } = await backstageApiClient.get("/analytics/skills", {
params: {
search
},
});
return data;
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { StatementOfWork } from "@/app/_domain/interfaces/StatementOfWork";
import useHoursHistoryGraphController from "./presenters/controllers/useHoursHistoryGraphController";
import Loading from "@/components/Loading";
import PieChart from "@/components/Charts/PieChart";
import HorizontalBarChart from "@/components/Charts/HorizontalBarChart";
import VerticalBarChart from "@/components/Charts/VerticalBarChart";
import Loading from "@/components/Loading";

import useHoursHistoryGraphController from "./presenters/controllers/useHoursHistoryGraphController";


type Props = {
statementOfWork: StatementOfWork;
Expand Down
60 changes: 20 additions & 40 deletions src/app/users/reports/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"use client";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { Grid, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { Fragment } from 'react';
import { Grid, Typography } from "@mui/material";

import HorizontalBarChart from '@/components/Charts/HorizontalBarChart';
import Loading from "@/components/Loading";

import Footer from "./presenters/components/Footer";
import SkillsList from './presenters/components/SkillsList';
import SkillsSearch from './presenters/components/SkillsSearch';
import UsersTable from './presenters/components/UsersTable';
import useReportsController from "./presenters/controllers/useReportsController";

const UsersDashboard = () => {
Expand All @@ -21,7 +19,9 @@ const UsersDashboard = () => {
onExpand,
selectedUser,
userSkills,
onKeyPress
onKeyPress,
onChangeSearch,
skillsAnalytics
} = useReportsController();

if (isLoading) {
Expand All @@ -43,45 +43,25 @@ const UsersDashboard = () => {
query={query}
setQuery={setQuery}
onKeyPress={onKeyPress}
onChangeSearch={onChangeSearch}
/>
</Grid>
</Grid>
<Grid xs={12} md={6} style={{ marginTop: "32px" }}>
<HorizontalBarChart
title="Skills Analytics"
chart={skillsAnalytics}
labelFormatter={skillsAnalytics.formatter}
/>
</Grid>
<Grid container justifyContent={"space-around"} display="flex" mt={5}>
<Grid item xs={12} md={12} pb={12}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell />
<TableCell component="th" scope="row">Users</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users?.map((user) => (
<Fragment key={user.id}>
<TableRow>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => onExpand(selectedUser == user.id ? null : user.id!.toString())}
>
{selectedUser == user.id ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{user.fullName}</TableCell>
<TableCell>{user.email}</TableCell>
</TableRow>
<SkillsList
user={user}
userSkills={userSkills}
selectedUser={selectedUser}
/>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
<UsersTable
users={users}
onExpand={onExpand}
selectedUser={selectedUser}
userSkills={userSkills}
/>
</Grid>
</Grid>
<Footer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import SearchIcon from '@mui/icons-material/Search';
import { IconButton, InputBase, Paper } from "@mui/material";

type Props = {
onChangeSearch: (query: string, delay: number) => void;
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onSearch: () => void;
query: string;
setQuery: (query: string) => void;
};

const SkillsSearch = ({ onSearch, query, setQuery, onKeyPress }: Props) => {
const SkillsSearch = ({ onSearch, query, setQuery, onKeyPress, onChangeSearch }: Props) => {
return (
<Paper
component="div"
Expand All @@ -23,7 +24,13 @@ const SkillsSearch = ({ onSearch, query, setQuery, onKeyPress }: Props) => {
placeholder="e.g. React, Node, Python"
inputProps={{ 'aria-label': 'search skills' }}
value={query}
onChange={(e) => setQuery(e.target.value)}
onChange={(e) => {
setQuery(e.target.value);
const length = e.target.value.length;
if (!length || length > 2) {
onChangeSearch(e.target.value, 1000);
}
}}
onKeyUp={onKeyPress}
autoFocus
/>
Expand Down
63 changes: 63 additions & 0 deletions src/app/users/reports/presenters/components/UsersTable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
import { Fragment } from 'react';

import { UserSkill } from '@/app/_domain/interfaces/Skill';
import { User } from '@/app/_domain/interfaces/User';

import SkillsList from '../SkillsList';

type Props = {
onExpand: (userId: string | null) => void;
selectedUser: string | null;
userSkills: UserSkill[] | undefined;
users: User[] | undefined;
};

const UsersTable = ({
users,
onExpand,
selectedUser,
userSkills
}: Props) => {
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell />
<TableCell component="th" scope="row">Users</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users?.map((user) => (
<Fragment key={user.id}>
<TableRow>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => onExpand(selectedUser == user.id ? null : user.id!.toString())}
>
{selectedUser == user.id ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{user.fullName}</TableCell>
<TableCell>{user.email}</TableCell>
</TableRow>
<SkillsList
user={user}
userSkills={userSkills}
selectedUser={selectedUser}
/>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
);
};

export default UsersTable;
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import debounce from "lodash.debounce";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

import tanstackKeys from "@/app/_domain/enums/tanstackKeys";
import { getSkillsAnalytics } from "@/app/_presenters/data/skills";
import { useAppStore } from "@/app/_presenters/data/store/store";
import { getUsers } from "@/app/_presenters/data/users";
import { getUserSkills } from "@/app/_presenters/data/userSkills";
Expand All @@ -15,7 +17,7 @@ const useReportsController = () => {
const [query, setQuery] = useState<string>("");
const [selectedUser, setSelectedUser] = useState<string | null>(null);

const { data: users, isLoading, refetch } = useQuery({
const { data: users, isLoading, refetch: usersRefetch } = useQuery({
queryKey: [tanstackKeys.Users, authKey],
queryFn: () => getUsers(true, false, query),
enabled: !!projectAuthKey,
Expand All @@ -29,6 +31,18 @@ const useReportsController = () => {
retry: false,
});

const { data: skillsAnalytics, refetch: skillsAnalyticsRefetch } = useQuery({
queryKey: [tanstackKeys.analyics, authKey],
queryFn: () => getSkillsAnalytics(query),
enabled: !!projectAuthKey,
retry: false,
});

const refetch = () => {
usersRefetch();
skillsAnalyticsRefetch();
}

const onSearch = () => {
setSelectedUser(null);
refetch();
Expand All @@ -51,6 +65,29 @@ const useReportsController = () => {
}
}, [authKey]);

const onChangeSearch = useCallback(debounce(refetch, 500), []);

const buildSkillsAnalytics = () => {
const chartColors = ["success", "info", "dark", "warning", "error", "secondary"];
const skillLevels = ['beginner', 'intermediate', 'advanced'];
return {
labels: skillsAnalytics?.map((skill) => skill.name),
datasets: skillLevels.map((level, index) => ({
label: level,
color: chartColors[index],
data: skillsAnalytics?.map((label) => {
const skill = skillsAnalytics?.find((pr) => pr.name === label.name);
const count = skill?.level.find((l) => l.name === level)?.count || 0;
return count;
}),
})),
formatter: (value: number) => {
if (value === 0) return "";
return value;
}
};
};

return {
users,
isLoading,
Expand All @@ -60,7 +97,9 @@ const useReportsController = () => {
userSkills,
onExpand,
selectedUser,
onKeyPress
onKeyPress,
onChangeSearch,
skillsAnalytics: buildSkillsAnalytics(),
};
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/Analytics/TimeEntries/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import Requirements from "./_presenters/components/Requirements";
import useTimeEntriesController from "./_presenters/controllers/useTimeEntriesController";

type Props = {
project?: Project;
defaultStatementOfWork?: StatementOfWork;
project?: Project;
};

const TimeEntries = ({ project, defaultStatementOfWork }: Props) => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Charts/HorizontalBarChart/configs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function configs(labels: any, datasets: any) {
function configs(labels: any, datasets: any, plugins: any = {}) {
return {
data: {
labels,
Expand All @@ -18,10 +18,10 @@ function configs(labels: any, datasets: any) {
anchor: "center",
formatter: (value: number) => {
if (value === 0) return "";

return value.toFixed(1);
},
},
...plugins,
},
scales: {
y: {
Expand Down
Loading

0 comments on commit e005b42

Please sign in to comment.