diff --git a/src/main/ipcs/ipcGitHub.ts b/src/main/ipcs/ipcGitHub.ts index ee6f774..f5be449 100644 --- a/src/main/ipcs/ipcGitHub.ts +++ b/src/main/ipcs/ipcGitHub.ts @@ -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'); @@ -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 @@ -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}`, @@ -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 }; + } +}); diff --git a/src/main/ipcs/preload.ts b/src/main/ipcs/preload.ts index 61fd610..5854620 100644 --- a/src/main/ipcs/preload.ts +++ b/src/main/ipcs/preload.ts @@ -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: { diff --git a/src/rendered/assets/gitHub/action-canceled.svg b/src/rendered/assets/gitHub/action-canceled.svg new file mode 100644 index 0000000..47c037d --- /dev/null +++ b/src/rendered/assets/gitHub/action-canceled.svg @@ -0,0 +1 @@ + diff --git a/src/rendered/assets/gitHub/action-done.svg b/src/rendered/assets/gitHub/action-done.svg new file mode 100644 index 0000000..5eedd15 --- /dev/null +++ b/src/rendered/assets/gitHub/action-done.svg @@ -0,0 +1 @@ + diff --git a/src/rendered/assets/gitHub/action-failed.svg b/src/rendered/assets/gitHub/action-failed.svg new file mode 100644 index 0000000..b865b3d --- /dev/null +++ b/src/rendered/assets/gitHub/action-failed.svg @@ -0,0 +1 @@ + diff --git a/src/rendered/assets/gitHub/action-in-progress.svg b/src/rendered/assets/gitHub/action-in-progress.svg new file mode 100644 index 0000000..4951b1c --- /dev/null +++ b/src/rendered/assets/gitHub/action-in-progress.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/rendered/assets/gitHub/action-pending.svg b/src/rendered/assets/gitHub/action-pending.svg new file mode 100644 index 0000000..e82ba4c --- /dev/null +++ b/src/rendered/assets/gitHub/action-pending.svg @@ -0,0 +1 @@ + diff --git a/src/rendered/assets/gitHub/action-queued.svg b/src/rendered/assets/gitHub/action-queued.svg new file mode 100644 index 0000000..48ce342 --- /dev/null +++ b/src/rendered/assets/gitHub/action-queued.svg @@ -0,0 +1 @@ + diff --git a/src/rendered/assets/gitHub/actions.svg b/src/rendered/assets/gitHub/actions.svg new file mode 100644 index 0000000..44de10b --- /dev/null +++ b/src/rendered/assets/gitHub/actions.svg @@ -0,0 +1,3 @@ + diff --git a/src/rendered/assets/gitHubIcons.tsx b/src/rendered/assets/gitHubIcons.tsx new file mode 100644 index 0000000..5aa3628 --- /dev/null +++ b/src/rendered/assets/gitHubIcons.tsx @@ -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; + } +}; diff --git a/src/rendered/components/Project/Project.styles.ts b/src/rendered/components/Project/Project.styles.ts index 6fa3af4..e89bc88 100644 --- a/src/rendered/components/Project/Project.styles.ts +++ b/src/rendered/components/Project/Project.styles.ts @@ -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; diff --git a/src/rendered/components/Project/Project.tsx b/src/rendered/components/Project/Project.tsx index fe46175..2a30af4 100644 --- a/src/rendered/components/Project/Project.tsx +++ b/src/rendered/components/Project/Project.tsx @@ -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; @@ -20,12 +22,16 @@ type Props = { export const Project: FC = ({ 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); @@ -57,76 +63,82 @@ export const Project: FC = ({ project }) => { } return ( - - - - {name} - {gitStatus?.organization ?? 'Local git'} - - - - - - - - - - - - - - {!behind && ( -