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 && (
-
- )}
- {Boolean(behind) && (
-
- )}
-
+
+
+
+ {name}
+ {gitStatus?.organization ?? 'Local git'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!behind && (
+
+ )}
+ {Boolean(behind) && (
+
+ )}
+
+ }
+ placement="bottom-end"
+ >
+
- }
- placement="bottom-end"
- >
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {Actions}
+ >
);
};
diff --git a/src/rendered/components/Project/components/Error/Error.tsx b/src/rendered/components/Project/components/Error/Error.tsx
index 3b3e4d4..97262df 100644
--- a/src/rendered/components/Project/components/Error/Error.tsx
+++ b/src/rendered/components/Project/components/Error/Error.tsx
@@ -1,7 +1,7 @@
import { Button, Tag } from '@blueprintjs/core';
import { FC } from 'react';
-import { Actions, Info, Root, Title, MiddleBlock } from '../../Project.styles';
+import { ProjectActions, Info, Root, Title, MiddleBlock } from '../../Project.styles';
type Props = {
name: string;
@@ -24,13 +24,13 @@ export const Error: FC = ({ name, removeAlert }) => (
-
+
-
+
);
diff --git a/src/rendered/components/Project/components/OpenInMenu/OpenInMenu.tsx b/src/rendered/components/Project/components/OpenInMenu/OpenInMenu.tsx
index 5b28258..6c35673 100644
--- a/src/rendered/components/Project/components/OpenInMenu/OpenInMenu.tsx
+++ b/src/rendered/components/Project/components/OpenInMenu/OpenInMenu.tsx
@@ -4,6 +4,7 @@ import { FaGithub } from 'react-icons/fa';
import { useAppSettings } from 'rendered/hooks/useAppSettings';
import { GitStatus, Project } from 'types/project';
+import { ActionsIcon } from 'rendered/assets/gitHubIcons';
import VSCode from '../../assets/VSCode.svg?react';
import Warp from '../../assets/Warp.svg?react';
@@ -30,7 +31,6 @@ export const OpenInMenu: FC = ({ project, gitStatus }) => {
selectedEditor?.editor === 'Visual Studio Code' ? : 'code';
const openInGitHub = (path: string = '') => {
- console.log('openInGitHub', path);
window.open(`https://github.com/${gitStatus.organization}/${project.name}${path}`, '_blank');
};
@@ -67,7 +67,7 @@ export const OpenInMenu: FC = ({ project, gitStatus }) => {
onClick={() => openInGitHub('/')}
/>
}
text={`Actions`}
onClick={() => openInGitHub('/actions')}
/>
diff --git a/src/rendered/components/Project/components/QuickActions/QuickActions.tsx b/src/rendered/components/Project/components/QuickActions/QuickActions.tsx
index bda113c..0faafba 100644
--- a/src/rendered/components/Project/components/QuickActions/QuickActions.tsx
+++ b/src/rendered/components/Project/components/QuickActions/QuickActions.tsx
@@ -2,6 +2,7 @@ import { Button, ButtonGroup, Classes, Popover } from '@blueprintjs/core';
import { FC, useState } from 'react';
import { GitStatus, Project } from 'types/project';
+import { ActionsIcon } from 'rendered/assets/gitHubIcons';
import { GitMenu } from '../GitMenu';
import { OpenInMenu } from '../OpenInMenu';
@@ -10,12 +11,14 @@ import { StyledFaCopy, StyledFaRegCopy } from './QuickActions.styles';
const size = 16;
type Props = {
+ actions: boolean;
gitStatus: GitStatus;
loading?: boolean;
project: Project;
+ toggleActions: () => void;
};
-export const QuickActions: FC = ({ project, gitStatus, loading }) => {
+export const QuickActions: FC = ({ project, gitStatus, loading, toggleActions, actions }) => {
const [copyIcon, setCopyIcon] = useState();
const copyToClipboard = () => {
@@ -65,6 +68,14 @@ export const QuickActions: FC = ({ project, gitStatus, loading }) => {
title="Git actions"
/>
+
+ }
+ loading={loading}
+ title="Git actions"
+ onClick={toggleActions}
+ />
);
};
diff --git a/src/rendered/components/Project/components/Workflow/Workflow.styles.ts b/src/rendered/components/Project/components/Workflow/Workflow.styles.ts
new file mode 100644
index 0000000..05659f0
--- /dev/null
+++ b/src/rendered/components/Project/components/Workflow/Workflow.styles.ts
@@ -0,0 +1,62 @@
+import { Colors } from '@blueprintjs/core';
+import styled from 'styled-components';
+
+export const Root = styled.div`
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 20px;
+ padding: 6px 15px 6px 20px;
+ background-color: ${Colors.LIGHT_GRAY4};
+ margin: 2px 0px;
+
+ & + & {
+ margin-top: 0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ background-color: ${Colors.DARK_GRAY2};
+ }
+`;
+
+export const Status = styled.div``;
+
+export const Title = styled.div`
+ display: flex;
+ text-align: left;
+ justify-content: start;
+ gap: 15px;
+ align-items: center;
+ width: 430px;
+`;
+
+export const TitleText = styled.div`
+ font-size: 13px;
+ display: flex;
+ flex-direction: column;
+`;
+
+export const TitleDescription = styled.div`
+ margin-top: -2px;
+ font-size: 11px;
+ font-weight: 300;
+
+ @media (prefers-color-scheme: dark) {
+ color: ${Colors.GRAY3};
+ }
+`;
+
+export const Event = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ margin-left: 10px;
+ font-weight: 300;
+ width: 200px;
+
+ @media (prefers-color-scheme: dark) {
+ color: ${Colors.GRAY3};
+ }
+`;
diff --git a/src/rendered/components/Project/components/Workflow/Workflow.tsx b/src/rendered/components/Project/components/Workflow/Workflow.tsx
new file mode 100644
index 0000000..d4ebaa9
--- /dev/null
+++ b/src/rendered/components/Project/components/Workflow/Workflow.tsx
@@ -0,0 +1,47 @@
+import { FC } from 'react';
+import { Button, Tag } from '@blueprintjs/core';
+
+import { getStatusIcon } from 'rendered/assets/gitHubIcons';
+import { Run } from 'types/gitHub';
+
+import { Root, Title, TitleDescription, Event, Status, TitleText } from './Workflow.styles';
+
+type Props = {
+ run: Run;
+};
+
+export const Workflow: FC = ({
+ run: { display_title, name, run_number, head_branch, event, status, html_url }
+}) => {
+ const Icon = getStatusIcon(status);
+
+ const openInBrowser = () => {
+ window.open(html_url, '_blank');
+ };
+
+ return (
+
+
+
+
+
+
+
+ {display_title}
+
+ {name} #{run_number}
+
+
+
+
+
+ {head_branch} {event}
+
+
+
+
+ );
+};
diff --git a/src/rendered/components/Project/components/Workflow/index.ts b/src/rendered/components/Project/components/Workflow/index.ts
new file mode 100644
index 0000000..98ea89b
--- /dev/null
+++ b/src/rendered/components/Project/components/Workflow/index.ts
@@ -0,0 +1 @@
+export { Workflow } from './Workflow';
diff --git a/src/rendered/components/Project/hooks/useActions.tsx b/src/rendered/components/Project/hooks/useActions.tsx
new file mode 100644
index 0000000..65065ba
--- /dev/null
+++ b/src/rendered/components/Project/hooks/useActions.tsx
@@ -0,0 +1,55 @@
+import { useEffect, useMemo, useState } from 'react';
+
+import { GitStatus, Project } from 'types/project';
+
+import { Workflow } from '../components/Workflow';
+
+export const useActions = (gitStatus: GitStatus, project: Project) => {
+ const [runs, setRuns] = useState([]);
+ const [showActions, setShowActions] = useState(false);
+
+ const getActions = async () => {
+ const savedOrigin = localStorage.getItem(`GitResetModal:origin-${project.id}`);
+ const filterBy = [gitStatus.branchSummary.current];
+ if (savedOrigin) filterBy.push(savedOrigin);
+
+ const res = await window.bridge.gitAPI.getAction(project.id, filterBy);
+
+ if (!res.success) {
+ setShowActions(false);
+ return;
+ }
+
+ setRuns(res.runs ?? []);
+ };
+
+ const toggleActions = () => setShowActions(!showActions);
+
+ useEffect(() => {
+ if (!showActions || !gitStatus?.branchSummary.current || !project.id) return;
+ getActions();
+ }, [gitStatus, project, showActions]);
+
+ const Actions = useMemo(
+ () =>
+ showActions &&
+ runs.length > 0 && (
+ <>
+ {runs.map((run: any) => (
+
+ ))}
+ >
+ ),
+ [runs, showActions]
+ );
+
+ return {
+ Actions,
+ getActions,
+ showActions,
+ toggleActions
+ };
+};
diff --git a/src/types/gitHub.ts b/src/types/gitHub.ts
new file mode 100644
index 0000000..71d01f5
--- /dev/null
+++ b/src/types/gitHub.ts
@@ -0,0 +1,8 @@
+import { GetResponseDataTypeFromEndpointMethod } from '@octokit/types';
+import { Octokit } from 'octokit';
+
+const octokit = new Octokit();
+
+export type Run = GetResponseDataTypeFromEndpointMethod<
+ typeof octokit.rest.actions.listWorkflowRunsForRepo
+>['workflow_runs'][0];