Skip to content
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

Improve appearance of inspection section #1016

Merged
merged 14 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions backend/api/Controllers/AreaController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,35 @@ public async Task<ActionResult<AreaResponse>> GetAreaById([FromRoute] string id)
}
}

/// <summary>
/// Lookup area by specified deck id.
/// </summary>
[HttpGet]
[Authorize(Roles = Role.Any)]
[Route("deck/{deckId}")]
[ProducesResponseType(typeof(AreaResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IList<AreaResponse>>> GetAreaByDeckId([FromRoute] string deckId)
{
try
{
var areas = await _areaService.ReadByDeckId(deckId);
if (!areas.Any())
return NotFound($"Could not find area for deck with id {deckId}");
mrica-equinor marked this conversation as resolved.
Show resolved Hide resolved

var response = areas.Select(area => new AreaResponse(area!));
return Ok(response);
}
catch (Exception e)
{
_logger.LogError(e, "Error during GET of areas from database");
throw;
}
}

/// <summary>
/// Lookup all the mission definitions related to an area
/// </summary>
Expand Down
47 changes: 47 additions & 0 deletions backend/api/Controllers/MissionDefinitionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,53 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters
return Ok(missionDefinitionResponses);
}

/// <summary>
/// List all consended mission definitions in the Flotilla database
/// </summary>
/// <remarks>
/// <para> This query gets all consended mission definitions </para>
/// </remarks>
[HttpGet("condensed")]
[Authorize(Roles = Role.Any)]
[ProducesResponseType(typeof(IList<MissionDefinitionResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<IList<CondensedMissionDefinitionResponse>>> GetCondensedMissionDefinitions(
[FromQuery] MissionDefinitionQueryStringParameters parameters
)
{
PagedList<MissionDefinition> missionDefinitions;
try
{
missionDefinitions = await _missionDefinitionService.ReadAll(parameters);
}
catch (InvalidDataException e)
{
_logger.LogError(e.Message);
return BadRequest(e.Message);
}

var metadata = new
{
missionDefinitions.TotalCount,
missionDefinitions.PageSize,
missionDefinitions.CurrentPage,
missionDefinitions.TotalPages,
missionDefinitions.HasNext,
missionDefinitions.HasPrevious
};

Response.Headers.Add(
QueryStringParameters.PaginationHeader,
JsonSerializer.Serialize(metadata)
);

var missionDefinitionResponses = missionDefinitions.Select(m => new CondensedMissionDefinitionResponse(m));
return Ok(missionDefinitionResponses);
}

/// <summary>
/// Lookup mission definition by specified id.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions backend/api/Services/AreaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public interface IAreaService

public abstract Task<Area?> ReadById(string id);

public abstract Task<IEnumerable<Area?>> ReadByDeckId(string deckId);

public abstract Task<IEnumerable<Area>> ReadByInstallation(string installationCode);

public abstract Task<Area?> ReadByInstallationAndName(string installationCode, string areaName);
Expand Down Expand Up @@ -73,6 +75,17 @@ private IQueryable<Area> GetAreas()
.FirstOrDefaultAsync(a => a.Id.Equals(id));
}

public async Task<IEnumerable<Area?>> ReadByDeckId(string deckId)
{
if (deckId == null)
return new List<Area>();

return await _context.Areas.Where(a =>
a.Deck != null && a.Deck.Id.Equals(deckId)
).Include(a => a.SafePositions).Include(a => a.Installation)
.Include(a => a.Plant).Include(a => a.Deck).ToListAsync();
}

public async Task<Area?> ReadByInstallationAndName(Installation? installation, string areaName)
{
if (installation == null)
Expand Down
22 changes: 10 additions & 12 deletions frontend/package-lock.json

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

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@types/react-dom": "^18.0.6",
"date-fns": "^2.29.2",
"ovenplayer": "^0.10.25",
"prettier": "^3.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
Expand Down Expand Up @@ -58,7 +59,6 @@
"@types/ovenplayer": "^0.10.1",
"@types/react-modal": "^3.13.1",
"@types/styled-components": "^5.1.25",
"@types/video.js": "^7.3.50",
"prettier": "^2.8.4"
"@types/video.js": "^7.3.50"
}
}
17 changes: 13 additions & 4 deletions frontend/src/api/ApiCaller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { tokenReverificationInterval } from 'components/Contexts/AuthProvider'
import { TaskStatus } from 'models/Task'
import { CreateCustomMission, CustomMissionQuery } from 'models/CustomMission'
import { MapMetadata } from 'models/MapMetadata'
import { CondensedMissionDefinition, EchoMissionDefinition, MissionDefinition } from 'models/MissionDefinition'
import { CondensedMissionDefinition, EchoMissionDefinition } from 'models/MissionDefinition'
import { EchoMission } from 'models/EchoMission'
import { MissionDefinitionUpdateForm } from 'models/MissionDefinitionUpdateForm'
import { Deck } from 'models/Deck'
Expand Down Expand Up @@ -194,8 +194,8 @@ export class BackendAPICaller {

static async getMissionDefinitions(
parameters: MissionDefinitionQueryParameters
): Promise<PaginatedResponse<MissionDefinition>> {
let path: string = 'missions/definitions?'
): Promise<PaginatedResponse<CondensedMissionDefinition>> {
let path: string = 'missions/definitions/condensed?'

// Always filter by currently selected installation
const installationCode: string | null = BackendAPICaller.installationCode
Expand All @@ -209,7 +209,7 @@ export class BackendAPICaller {
if (parameters.nameSearch) path = path + 'NameSearch=' + parameters.nameSearch + '&'
if (parameters.sourceType) path = path + 'SourceType=' + parameters.sourceType + '&'

const result = await BackendAPICaller.GET<MissionDefinition[]>(path).catch((e) => {
const result = await BackendAPICaller.GET<CondensedMissionDefinition[]>(path).catch((e) => {
console.error(`Failed to GET /${path}: ` + e)
throw e
})
Expand Down Expand Up @@ -431,6 +431,15 @@ export class BackendAPICaller {
return result.content
}

static async getAreasByDeckId(deckId: string): Promise<Area[]> {
const path: string = 'areas/deck/' + deckId
const result = await this.GET<Area[]>(path).catch((e) => {
console.error(`Failed to GET /${path}: ` + e)
throw e
})
return result.content
}

static async getDecks(): Promise<Deck[]> {
const path: string = 'decks'
const result = await this.GET<Deck[]>(path).catch((e) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Pages/FrontPage/FrontPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RobotStatusSection } from 'components/Pages/FrontPage/RobotCards/RobotS
import { FailedMissionAlertView } from './MissionOverview/FailedMissionAlertView'
import { Header } from 'components/Header/Header'
import styled from 'styled-components'
import { InspectionSection } from '../InspectionPage/InspectionSection'
import { InspectionOverviewSection } from '../InspectionPage/InspectionOverview'

const StyledFrontPage = styled.div`
display: grid;
Expand Down Expand Up @@ -48,7 +48,7 @@ export function FrontPage() {
</MissionsContent>
</HorizontalContent>
<RobotStatusSection refreshInterval={refreshInterval} />
<InspectionSection refreshInterval={refreshInterval} />
<InspectionOverviewSection refreshInterval={refreshInterval} />
</StyledFrontPage>
</>
)
Expand Down
146 changes: 146 additions & 0 deletions frontend/src/components/Pages/InspectionPage/DeckCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Deck } from 'models/Deck'
import { DeckAreas, DeckMissionType, Inspection, ScheduledMissionType } from './InspectionSection'
import { useLanguageContext } from 'components/Contexts/LanguageContext'
import { CardMissionInformation, StyledDict, compareInspections, getDeadlineInspection } from './InspectionUtilities'
import { Button, Icon, Tooltip, Typography } from '@equinor/eds-core-react'
import { Icons } from 'utils/icons'
import { tokens } from '@equinor/eds-tokens'
import { useEffect, useState } from 'react'
import { BackendAPICaller } from 'api/ApiCaller'

interface IDeckCardProps {
deckMissions: DeckMissionType
setSelectedDeck: React.Dispatch<React.SetStateAction<Deck | undefined>>
selectedDeck: Deck | undefined
ongoingMissions: ScheduledMissionType
handleScheduleAll: (inspections: Inspection[]) => void
}

export function DeckCards({
deckMissions,
setSelectedDeck,
selectedDeck,
ongoingMissions,
handleScheduleAll,
}: IDeckCardProps) {
const { TranslateText } = useLanguageContext()
const [areas, setAreas] = useState<DeckAreas>({})

const getCardColor = (deckId: string) => {
const inspections = deckMissions[deckId].inspections
if (inspections.length === 0) return 'gray'
const sortedInspections = inspections.sort(compareInspections)

if (sortedInspections.length === 0) return 'green'

const nextInspection = sortedInspections[0]

if (!nextInspection.deadline) {
if (!nextInspection.missionDefinition.inspectionFrequency) return 'gray'
else return 'red'
}

return getDeadlineInspection(nextInspection.deadline)
}

useEffect(() => {
const newAreas: DeckAreas = {}

Object.keys(deckMissions).forEach((deckId) => {
BackendAPICaller.getAreasByDeckId(deckMissions[deckId].deck.id).then((areas) => {
const formattedAreaNames = areas
.map((area) => {
return area.areaName.toLocaleUpperCase()
})
.sort()
.join(' | ')
newAreas[deckMissions[deckId].deck.id] = {
areaString: formattedAreaNames,
}
})
})
setAreas(newAreas)
}, [deckMissions])

return (
<>
<StyledDict.DeckCards>
{Object.keys(deckMissions).length > 0 ? (
Object.keys(deckMissions).map((deckId) => (
<StyledDict.DeckCard key={deckId}>
<StyledDict.Rectangle style={{ background: `${getCardColor(deckId)}` }} />
<StyledDict.Card
key={deckId}
onClick={
deckMissions[deckId].inspections.length > 0
? () => setSelectedDeck(deckMissions[deckId].deck)
: undefined
}
style={
selectedDeck === deckMissions[deckId].deck
? { border: `solid ${getCardColor(deckId)} 2px` }
: {}
}
>
<StyledDict.DeckText>
<StyledDict.TopDeckText>
<Typography variant={'body_short_bold'}>
{deckMissions[deckId].deck.deckName.toString()}
</Typography>
{deckMissions[deckId].inspections
.filter((i) =>
Object.keys(ongoingMissions).includes(i.missionDefinition.id)
)
.map((inspection) => (
<StyledDict.Content key={inspection.missionDefinition.id}>
<Icon name={Icons.Ongoing} size={16} />
{TranslateText('InProgress')}
</StyledDict.Content>
))}
</StyledDict.TopDeckText>
{Object.keys(areas).includes(deckId) && (
<Typography variant={'body_short'}>{areas[deckId].areaString}</Typography>
)}
{deckMissions[deckId].inspections && (
<CardMissionInformation deckId={deckId} deckMissions={deckMissions} />
)}
</StyledDict.DeckText>
<StyledDict.CardComponent>
<Tooltip
placement="top"
title={
deckMissions[deckId].inspections.length > 0
? ''
: TranslateText('No planned inspection')
}
>
<Button
disabled={deckMissions[deckId].inspections.length === 0}
variant="outlined"
onClick={() => handleScheduleAll(deckMissions[deckId].inspections)}
color="secondary"
>
<Icon
name={Icons.LibraryAdd}
color={deckMissions[deckId].inspections.length > 0 ? '' : 'grey'}
/>
<Typography color={tokens.colors.text.static_icons__secondary.rgba}>
{TranslateText('Queue the missions')}
</Typography>
</Button>
</Tooltip>
</StyledDict.CardComponent>
</StyledDict.Card>
</StyledDict.DeckCard>
))
) : (
<StyledDict.Placeholder>
<Typography variant="h4" color="disabled">
{TranslateText('No deck inspections available')}
</Typography>
</StyledDict.Placeholder>
)}
</StyledDict.DeckCards>
</>
)
}
Loading
Loading