Skip to content
36 changes: 33 additions & 3 deletions src/main/ipcs/ipcGitHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { settings } from '../settings';

const protectedBranches = ['master', 'main'];

const getClient = () => {
const octokit = () => {
const { gitHubToken } = settings.get('appSettings');
if (!gitHubToken) throw new Error('GitHub token not found');

Expand All @@ -30,7 +30,7 @@ ipcMain.handle('git:api:reset', async (_, id: string, origin: string, target: st
const { owner, repo } = await getRepoInfo(id);
if (!owner || !repo) throw new Error('Project not found');

const targetData = await getClient().rest.git.getRef({
const targetData = await octokit().rest.git.getRef({
owner,
ref: `heads/${target}`,
repo
Expand All @@ -39,7 +39,7 @@ ipcMain.handle('git:api:reset', async (_, id: string, origin: string, target: st
const sha = targetData.data?.object?.sha;
if (!sha) throw new Error('Target branch not found');

getClient().rest.git.updateRef({
octokit().rest.git.updateRef({
force: true,
owner,
ref: `heads/${origin}`,
Expand All @@ -52,3 +52,33 @@ ipcMain.handle('git:api:reset', async (_, id: string, origin: string, target: st
return { message: e.message, success: false };
}
});

ipcMain.handle('git:api:getAction', async (_, id: string, filterBy: string[]) => {
try {
const { owner, repo } = await getRepoInfo(id);
if (!owner || !repo) throw new Error('Project not found');

const { data } = await octokit().rest.actions.listWorkflowRunsForRepo({
owner,
per_page: 3,
repo
});

if (data.total_count < 1) {
return { message: 'No running actions', success: true };
}

// Filter by branch and only from last 24 hours
const runs = data.workflow_runs
.filter((run) => filterBy.includes(run.head_branch))
.filter((run) => new Date(run.created_at).getTime() > Date.now() - 86400000);

if (runs.length < 1) {
return { message: 'No actions for this branch', success: false };
}

return { data, filterBy, runs, success: true };
} catch (e) {
return { message: e.message, success: false };
}
});
1 change: 1 addition & 0 deletions src/main/ipcs/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const bridge = {
reset: (id: string, target: string, force: boolean) => ipcRenderer.invoke('git:reset', id, target, force)
},
gitAPI: {
getAction: (id: string, filterBy: string[]) => ipcRenderer.invoke('git:api:getAction', id, filterBy),
reset: (id: string, origin: string, target: string) => ipcRenderer.invoke('git:api:reset', id, origin, target)
},
launch: {
Expand Down
1 change: 1 addition & 0 deletions src/rendered/assets/gitHub/action-canceled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/rendered/assets/gitHub/action-done.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/rendered/assets/gitHub/action-failed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/rendered/assets/gitHub/action-in-progress.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/rendered/assets/gitHub/action-pending.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/rendered/assets/gitHub/action-queued.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/rendered/assets/gitHub/actions.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 90 additions & 0 deletions src/rendered/assets/gitHubIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Colors } from '@blueprintjs/core';
import styled, { keyframes } from 'styled-components';

import { Run } from 'types/gitHub';

import actionDone from './gitHub/action-done.svg?react';
import actionFailed from './gitHub/action-failed.svg?react';
import actionCanceled from './gitHub/action-canceled.svg?react';
import actionInProgressIcon from './gitHub/action-in-progress.svg?react';
import actionPendingIcon from './gitHub/action-pending.svg?react';
import ActionQueuedIcon from './gitHub/action-queued.svg?react';
import actions from './gitHub/actions.svg?react';

export const ActionsIcon = styled(actions)`
fill: ${Colors.GRAY1};
@media (prefers-color-scheme: dark) {
fill: ${Colors.GRAY4};
}
`;

export const ActionsCanceledIcon = styled(actionCanceled)`
fill: ${Colors.GRAY1};
@media (prefers-color-scheme: dark) {
fill: ${Colors.GRAY4};
}
`;

export const ActionDoneIcon = styled(actionDone)`
fill: #1a7f37;
@media (prefers-color-scheme: dark) {
fill: #57ab5a;
}
`;

export const ActionFailedIcon = styled(actionFailed)`
fill: #d1242f;
@media (prefers-color-scheme: dark) {
fill: #e5534b;
}
`;

const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;

export const ActionInProgressIcon = styled(actionInProgressIcon)`
animation: ${rotate} 1s linear infinite;
height: 16px;
`;

const blink = keyframes`
0% {
opacity: 0.2;
}
50% {
opacity: 1;
}
100% {
opacity: 0.2;
}
`;

export const ActionPendingIcon = styled(actionPendingIcon)`
animation: ${blink} 1.5s linear infinite;
height: 16px;
`;

export const getStatusIcon = (status: Run['status']) => {
switch (status) {
case 'completed':
return ActionDoneIcon;
case 'failed':
return ActionFailedIcon;
case 'cancelled':
return ActionsCanceledIcon;
case 'in_progress':
return ActionInProgressIcon;
case 'queued':
return ActionQueuedIcon;
case 'pending':
return ActionPendingIcon;
default:
return ActionsIcon;
}
};
2 changes: 1 addition & 1 deletion src/rendered/components/Project/Project.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const MiddleBlock = styled.div`
gap: 10px;
`;

export const Actions = styled.div`
export const ProjectActions = styled.div`
display: flex;
position: relative;
flex-direction: row-reverse;
Expand Down
160 changes: 86 additions & 74 deletions src/rendered/components/Project/Project.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
/* eslint-disable react/jsx-max-depth */
import { Button, ButtonGroup, Classes, Colors, Popover } from '@blueprintjs/core';
import { FC, useState } from 'react';

import { useGit } from 'rendered/hooks/useGit';
import { useModal } from 'rendered/hooks/useModal';
import { useMountEffect } from 'rendered/hooks/useMountEffect';
import { Project as IProject } from 'types/project';
import { useModal } from 'rendered/hooks/useModal';

import { GitStatusGroup } from '../GitStatusGroup';
import { Actions, Info, InfoText, MiddleBlock, RepoInfo, Root, StyledSpinner, Title } from './Project.styles';
import { Info, InfoText, MiddleBlock, ProjectActions, RepoInfo, Root, StyledSpinner, Title } from './Project.styles';
import { CheckoutBranch } from './components/CheckoutBranch';
import { Error } from './components/Error';
import { ProjectMenu } from './components/ProjectMenu';
import { QuickActions } from './components/QuickActions';
import { useActions } from './hooks/useActions';

type Props = {
project: IProject;
Expand All @@ -20,12 +22,16 @@ type Props = {
export const Project: FC<Props> = ({ project }) => {
const { gitStatus, getStatus, loading, pull } = useGit();
const { openModal } = useModal();

const [pullLoading, setPullLoading] = useState(false);

const { Actions, showActions, toggleActions, getActions } = useActions(gitStatus, project);

const { group, id, name } = project;

const runGetStatus = () => getStatus(id);
const updateProject = () => {
showActions && getActions();
getStatus(id);
};

const runPull = async () => {
setPullLoading(true);
Expand Down Expand Up @@ -57,76 +63,82 @@ export const Project: FC<Props> = ({ project }) => {
}

return (
<Root>
<Info>
<InfoText className={!gitStatus && Classes.SKELETON}>
<Title>{name}</Title>
<RepoInfo>{gitStatus?.organization ?? 'Local git'}</RepoInfo>
</InfoText>

<GitStatusGroup
gitStatus={gitStatus}
name={name}
/>
</Info>

<MiddleBlock>
<CheckoutBranch
getStatus={runGetStatus}
gitStatus={gitStatus}
id={id}
name={name}
/>

<QuickActions
gitStatus={gitStatus}
project={project}
/>
</MiddleBlock>

<Actions className={!gitStatus && Classes.SKELETON}>
<ButtonGroup large>
{!behind && (
<Button
icon="refresh"
onClick={runGetStatus}
/>
)}
{Boolean(behind) && (
<Button
icon="arrow-down"
intent="warning"
loading={pullLoading}
onClick={runPull}
/>
)}
<Popover
content={
<ProjectMenu
getStatus={runGetStatus}
gitStatus={gitStatus}
group={group}
id={id}
name={name}
pull={runPull}
removeProject={removeAlert}
<>
<Root>
<Info>
<InfoText className={!gitStatus && Classes.SKELETON}>
<Title>{name}</Title>
<RepoInfo>{gitStatus?.organization ?? 'Local git'}</RepoInfo>
</InfoText>

<GitStatusGroup
gitStatus={gitStatus}
name={name}
/>
</Info>

<MiddleBlock>
<CheckoutBranch
getStatus={updateProject}
gitStatus={gitStatus}
id={id}
name={name}
/>

<QuickActions
actions={showActions}
gitStatus={gitStatus}
project={project}
toggleActions={toggleActions}
/>
</MiddleBlock>

<ProjectActions className={!gitStatus && Classes.SKELETON}>
<ButtonGroup large>
{!behind && (
<Button
icon="refresh"
onClick={updateProject}
/>
)}
{Boolean(behind) && (
<Button
icon="arrow-down"
intent="warning"
loading={pullLoading}
onClick={runPull}
/>
)}
<Popover
content={
<ProjectMenu
getStatus={updateProject}
gitStatus={gitStatus}
group={group}
id={id}
name={name}
pull={runPull}
removeProject={removeAlert}
/>
}
placement="bottom-end"
>
<Button
icon="caret-down"
intent={ahead || behind ? 'warning' : 'none'}
/>
}
placement="bottom-end"
>
<Button
icon="caret-down"
intent={ahead || behind ? 'warning' : 'none'}
/>
</Popover>
</ButtonGroup>

<StyledSpinner
color={Colors.ORANGE1}
icon="dot"
loading={loading}
/>
</Actions>
</Root>
</Popover>
</ButtonGroup>

<StyledSpinner
color={Colors.ORANGE1}
icon="dot"
loading={loading}
/>
</ProjectActions>
</Root>

{Actions}
</>
);
};
Loading