-
Notifications
You must be signed in to change notification settings - Fork 9
3825-initial-commit #3826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/rules-dashboard
Are you sure you want to change the base?
3825-initial-commit #3826
Changes from all commits
5d79a30
ff9c391
92c3f9e
cb04807
aa8f142
b050462
75e138f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,36 @@ | ||
| // write api functions below here! | ||
| /* | ||
| * This file is part of NER's FinishLine and licensed under GNU AGPLv3. | ||
| * See the LICENSE file in the repository root folder for details. | ||
| */ | ||
|
|
||
| import axios from '../utils/axios'; | ||
| import { Rule } from 'shared'; | ||
| import { apiUrls } from '../utils/urls'; | ||
|
|
||
| /** | ||
| * Gets all top-level rules (rules with no parent) for a ruleset | ||
| */ | ||
| export const getTopLevelRules = (rulesetId: string) => { | ||
| return axios.get<Rule[]>(apiUrls.rulesTopLevel(rulesetId)); | ||
| }; | ||
|
|
||
| /** | ||
| * Gets all child rules of a specific rule | ||
| */ | ||
| export const getChildRules = (ruleId: string) => { | ||
| return axios.get<Rule[]>(apiUrls.rulesChildRules(ruleId)); | ||
| }; | ||
|
|
||
| /** | ||
| * Toggles team assignment for a rule | ||
| */ | ||
| export const toggleRuleTeam = (ruleId: string, teamId: string) => { | ||
| return axios.post<Rule>(apiUrls.rulesToggleTeam(ruleId), { teamId }); | ||
| }; | ||
|
|
||
| /** | ||
| * Gets all rules assigned to a team for a specific ruleset type | ||
| */ | ||
| export const getTeamRulesInRulesetType = (rulesetTypeId: string, teamId: string) => { | ||
| return axios.get<Rule[]>(apiUrls.rulesTeamRulesInRulesetType(rulesetTypeId, teamId)); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,45 @@ | ||
| // write hooks below here! | ||
| /* | ||
| * This file is part of NER's FinishLine and licensed under GNU AGPLv3. | ||
| * See the LICENSE file in the repository root folder for details. | ||
| */ | ||
|
|
||
| import { useQuery, useMutation, useQueryClient } from 'react-query'; | ||
| import { Rule } from 'shared'; | ||
| import { getTopLevelRules, getChildRules, toggleRuleTeam, getTeamRulesInRulesetType } from '../apis/rules.api'; | ||
|
|
||
| export const useGetTopLevelRules = (rulesetId: string) => { | ||
| return useQuery<Rule[], Error>(['rules', 'top-level', rulesetId], async () => { | ||
| const { data } = await getTopLevelRules(rulesetId); | ||
| return data; | ||
| }); | ||
| }; | ||
|
|
||
| export const useGetChildRules = (ruleId: string) => { | ||
| return useQuery<Rule[], Error>(['rules', 'children', ruleId], async () => { | ||
| const { data } = await getChildRules(ruleId); | ||
| return data; | ||
| }); | ||
| }; | ||
|
|
||
| export const useToggleRuleTeam = () => { | ||
| const queryClient = useQueryClient(); | ||
| return useMutation<Rule, Error, { ruleId: string; teamId: string }>( | ||
| ['rules', 'toggle-team'], | ||
| async ({ ruleId, teamId }) => { | ||
| const { data } = await toggleRuleTeam(ruleId, teamId); | ||
| return data; | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries(['rules']); | ||
| } | ||
| } | ||
| ); | ||
| }; | ||
|
|
||
| export const useGetTeamRulesInRulesetType = (rulesetTypeId: string, teamId: string) => { | ||
| return useQuery<Rule[], Error>(['rules', 'team-rules', rulesetTypeId, teamId], async () => { | ||
| const { data } = await getTeamRulesInRulesetType(rulesetTypeId, teamId); | ||
| return data; | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,278 @@ | ||
| /* | ||
| * This file is part of NER's FinishLine and licensed under GNU AGPLv3. | ||
| * See the LICENSE file in the repository root folder for details. | ||
| */ | ||
|
|
||
| import { | ||
| Box, | ||
| Chip, | ||
| Paper, | ||
| Table, | ||
| TableBody, | ||
| TableCell, | ||
| TableContainer, | ||
| TableRow, | ||
| Typography, | ||
| useTheme | ||
| } from '@mui/material'; | ||
| import { useState } from 'react'; | ||
| import { Rule, TeamPreview } from 'shared'; | ||
| import { useAllTeams } from '../../hooks/teams.hooks'; | ||
| import LoadingIndicator from '../../components/LoadingIndicator'; | ||
| import ErrorPage from '../ErrorPage'; | ||
| import { useHistory } from 'react-router-dom'; | ||
| import { routes } from '../../utils/routes'; | ||
| import { useToast } from '../../hooks/toasts.hooks'; | ||
| import { NERButton } from '../../components/NERButton'; | ||
| import RuleRow from './RuleRow'; | ||
|
|
||
| /* | ||
| * Props for the assign rules tab. | ||
| */ | ||
| interface AssignRulesTabProps { | ||
| rules: Rule[]; | ||
| } | ||
|
|
||
| /* | ||
| * Props for the team row. | ||
| */ | ||
| interface TeamRowProps { | ||
| team: TeamPreview; | ||
| isSelected: boolean; | ||
| onClick: () => void; | ||
| } | ||
|
|
||
| /** | ||
| * Row component for displaying a team in the teams table. | ||
| */ | ||
| const TeamRow: React.FC<TeamRowProps> = ({ team, isSelected, onClick }) => { | ||
| return ( | ||
| <TableRow | ||
| onClick={onClick} | ||
| sx={{ | ||
| borderBottom: '1px solid #7d7d7d', | ||
| backgroundColor: isSelected ? '#b36b6b' : '#CECECE', | ||
| '&:hover': { backgroundColor: isSelected ? '#a05858' : '#5e5e5e' }, | ||
| cursor: 'pointer', | ||
| '&:last-child': { borderBottom: 'none' } | ||
| }} | ||
| > | ||
| <TableCell | ||
| sx={{ | ||
| fontSize: '16px', | ||
| padding: '8px 16px', | ||
| backgroundColor: 'inherit', | ||
| borderBottom: 'none', | ||
| color: '#000000' | ||
| }} | ||
| > | ||
| {team.teamName} | ||
| </TableCell> | ||
| </TableRow> | ||
| ); | ||
| }; | ||
|
|
||
| /** | ||
| * Tab component for assigning rules to teams. | ||
| * Displays teams and rules side-by-side for selection. | ||
| */ | ||
| const AssignRulesTab: React.FC<AssignRulesTabProps> = ({ rules }) => { | ||
| const theme = useTheme(); | ||
| const history = useHistory(); | ||
| const toast = useToast(); | ||
| const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null); | ||
| const [assignments, setAssignments] = useState<Set<string>>(new Set()); | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const [originalAssignments, setOriginalAssignments] = useState<Set<string>>(new Set()); | ||
|
|
||
| const { data: teams, isLoading: teamsLoading, isError: teamsError, error: teamsErrorData } = useAllTeams(); | ||
|
|
||
| // TODO: Fetch all team assignments on mount and populate originalAssignments and assignments | ||
| // Not implemented yet since we do not use an actual ruleset yet | ||
|
|
||
| const handleTeamSelect = (teamId: string) => setSelectedTeamId(teamId); | ||
|
|
||
| const isRuleAssigned = (ruleId: string) => { | ||
| if (!selectedTeamId) return false; | ||
| return assignments.has(`${selectedTeamId}:${ruleId}`); | ||
| }; | ||
|
|
||
| const getAssignedTeamNames = (ruleId: string): string[] => { | ||
| if (!teams) return []; | ||
| const assignedTeamIds = [...assignments].filter((key) => key.endsWith(`:${ruleId}`)).map((key) => key.split(':')[0]); | ||
| return teams.filter((t) => assignedTeamIds.includes(t.teamId)).map((t) => t.teamName); | ||
| }; | ||
|
|
||
| const renderTeamTags = (ruleId: string) => { | ||
| const teamNames = getAssignedTeamNames(ruleId); | ||
| if (teamNames.length === 0) return null; | ||
| return ( | ||
| <Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', justifyContent: 'flex-end' }}> | ||
| {teamNames.map((name) => ( | ||
| <Chip | ||
| key={name} | ||
| label={name} | ||
| size="small" | ||
| sx={{ | ||
| height: '18px', | ||
| fontSize: '11px', | ||
| backgroundColor: theme.palette.background.paper, | ||
| color: theme.palette.text.primary | ||
| }} | ||
| /> | ||
| ))} | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| const handleRuleToggle = (ruleId: string) => { | ||
| if (!selectedTeamId) { | ||
| toast.error('Please select a team first'); | ||
| return; | ||
| } | ||
|
|
||
| const key = `${selectedTeamId}:${ruleId}`; | ||
| const newAssignments = new Set(assignments); | ||
| if (assignments.has(key)) { | ||
| newAssignments.delete(key); | ||
| } else { | ||
| newAssignments.add(key); | ||
| } | ||
| setAssignments(newAssignments); | ||
| }; | ||
|
|
||
| const handleSaveAndExit = () => { | ||
| const toAdd = [...assignments].filter((key) => !originalAssignments.has(key)); | ||
| const toRemove = [...originalAssignments].filter((key) => !assignments.has(key)); | ||
|
|
||
| // TODO: Save changes via backend | ||
| if (toAdd.length > 0 || toRemove.length > 0) { | ||
| toast.success(`Placeholder: Would save ${toAdd.length} additions and ${toRemove.length} removals`); | ||
| } | ||
|
|
||
| history.push(routes.RULES); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we go back to ruleset page instead of the ruleset types view |
||
| }; | ||
|
|
||
| if (teamsLoading) { | ||
| return <LoadingIndicator />; | ||
| } | ||
|
|
||
| if (teamsError) { | ||
| return <ErrorPage message={teamsErrorData?.message} error={teamsErrorData} />; | ||
| } | ||
|
|
||
| const topLevelRules = rules.filter((rule) => !rule.parentRule); | ||
|
|
||
| return ( | ||
| <Box sx={{ paddingBottom: '100px' }}> | ||
| <Box sx={{ display: 'flex', gap: 4 }}> | ||
| {/* Teams Column */} | ||
| <Box sx={{ flex: 1 }}> | ||
| <Typography variant="h4" sx={{ mb: 2, color: '#ffffff' }}> | ||
| Teams: | ||
| </Typography> | ||
| <TableContainer | ||
| component={Paper} | ||
| sx={{ | ||
| borderRadius: '8px', | ||
| overflow: 'hidden', | ||
| maxHeight: 'calc(100vh - 300px)', | ||
| overflowY: 'auto' | ||
| }} | ||
| > | ||
| <Table sx={{ borderCollapse: 'collapse' }}> | ||
| <TableBody sx={{ backgroundColor: '#CECECE' }}> | ||
| {teams?.map((team) => ( | ||
| <TeamRow | ||
| key={team.teamId} | ||
| team={team} | ||
| isSelected={selectedTeamId === team.teamId} | ||
| onClick={() => handleTeamSelect(team.teamId)} | ||
| /> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| </TableContainer> | ||
| </Box> | ||
|
|
||
| {/* Rules Column */} | ||
| <Box sx={{ flex: 1 }}> | ||
| <Typography variant="h4" sx={{ mb: 2, color: '#ffffff' }}> | ||
| Rules: | ||
| </Typography> | ||
| <TableContainer | ||
| component={Paper} | ||
| sx={{ | ||
| borderRadius: '8px', | ||
| overflow: 'hidden', | ||
| maxHeight: 'calc(100vh - 300px)', | ||
| overflowY: 'auto' | ||
| }} | ||
| > | ||
| <Table sx={{ borderCollapse: 'collapse' }}> | ||
| <TableBody sx={{ backgroundColor: '#CECECE' }}> | ||
| {topLevelRules.map((rule) => ( | ||
| <RuleRow | ||
| key={rule.ruleId} | ||
| rule={rule} | ||
| allRules={rules} | ||
| backgroundColor={(r) => { | ||
| const isLeaf = r.subRuleIds.length === 0; | ||
| return isLeaf && isRuleAssigned(r.ruleId) ? '#b36b6b' : '#CECECE'; | ||
| }} | ||
| hoverColor={(r) => { | ||
| const isLeaf = r.subRuleIds.length === 0; | ||
| return isLeaf && isRuleAssigned(r.ruleId) ? '#a05858' : '#5e5e5e'; | ||
| }} | ||
| textColor="#000000" | ||
| onRowClick={(r) => handleRuleToggle(r.ruleId)} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its a bit odd that we can't click the right side of the ruleRow to select but not high priority to fix |
||
| middleContent={() => null} | ||
| rightContent={(r) => renderTeamTags(r.ruleId)} | ||
| verticalPadding="8px" | ||
| leftWidth="70%" | ||
| middleWidth="0%" | ||
| rightWidth="30%" | ||
| /> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| </TableContainer> | ||
| </Box> | ||
| </Box> | ||
|
|
||
| {/* Save & Exit Button */} | ||
| <Box | ||
| sx={{ | ||
| backgroundColor: '#121313', | ||
| position: 'fixed', | ||
| bottom: 0, | ||
| left: 0, | ||
| right: 0, | ||
| width: '100%' | ||
| }} | ||
| > | ||
| <Box | ||
| sx={{ | ||
| borderBottom: '2px solid white', | ||
| mb: 2, | ||
| ml: '30px' | ||
| }} | ||
| /> | ||
| <Box sx={{ display: 'flex', justifyContent: 'flex-end', pr: '30px', pb: 1 }}> | ||
| <NERButton | ||
| variant="contained" | ||
| onClick={handleSaveAndExit} | ||
| sx={{ | ||
| backgroundColor: '#dd514c', | ||
| '&:hover': { backgroundColor: '#c74340' } | ||
| }} | ||
| > | ||
| Save & Exit | ||
| </NERButton> | ||
| </Box> | ||
| </Box> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default AssignRulesTab; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like if there were no changes there should be like a toast message saying 'No rules selected to assign' instead of still jumping back to the other page - but up to you