Merge branch 'preview' of github.com:makeplane/plane into develop

This commit is contained in:
sriram veeraghanta 2024-03-14 12:42:33 +05:30
commit 80761d3507
4 changed files with 199 additions and 135 deletions

View File

@ -1,82 +1,79 @@
# 1-Click Self-Hosting # One-click deploy
In this guide, we will walk you through the process of setting up a 1-click self-hosted environment. Self-hosting allows you to have full control over your applications and data. It's a great way to ensure privacy, control, and customization. Deployment methods for Plane have improved significantly to make self-managing super-easy. One of those is a single-line-command installation of Plane.
Let's get started! This short guide will guide you through the process, the background tasks that run with the command for the Community, One, and Enterprise editions, and the post-deployment configuration options available to you.
## Installing Plane ### Requirements
Installing Plane is a very easy and minimal step process. - Operating systems: Debian, Ubuntu, CentOS
- Supported CPU architectures: AMD64, ARM64, x86_64, AArch64
### Prerequisite ### Download the latest stable release
- Operating System (latest): Debian / Ubuntu / Centos Run ↓ on any CLI.
- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64
### Downloading Latest Stable Release
``` ```
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh -
``` ```
<details> ### Download the Preview release
<summary>Downloading Preview Release</summary>
`Preview` builds do not support ARM64, AArch64 CPU architectures
Run ↓ on any CLI.
``` ```
export BRANCH=preview export BRANCH=preview
curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh - curl -fsSL https://raw.githubusercontent.com/makeplane/plane/preview/deploy/1-click/install.sh | sh -
``` ```
NOTE: `Preview` builds do not support ARM64/AARCH64 CPU architecture
</details>
--
Expect this after a successful install
![Install Output](images/install.png)
Access the application on a browser via http://server-ip-address
--- ---
### Get Control of your Plane Server Setup ### Successful installation
Plane App is available via the command `plane-app`. Running the command `plane-app --help` helps you to manage Plane You should see ↓ if there are no hitches. That output will also list the IP address you can use to access your Plane instance.
![Install Output](images/install.png)
---
### Manage your Plane instance
Use `plane-app` [OPERATOR] to manage your Plane instance easily. Get a list of all operators with `plane-app ---help`.
![Plane Help](images/help.png) ![Plane Help](images/help.png)
<ins>Basic Operations</ins>: 1. Basic operators
1. Start Server using `plane-app start` 1. `plane-app start` starts the Plane server.
1. Stop Server using `plane-app stop` 2. `plane-app restart` restarts the Plane server.
1. Restart Server using `plane-app restart` 3. `plane-app stop` stops the Plane server.
<ins>Advanced Operations</ins>: 2. Advanced operators
1. Configure Plane using `plane-app --configure`. This will give you options to modify `plane-app --configure` will show advanced configurators.
- NGINX Port (default 80) - Change your proxy or listening port
- Domain Name (default is the local server public IP address) <br>Default: 80
- File Upload Size (default 5MB) - Change your domain name
- External Postgres DB Url (optional - default empty) <br>Default: Deployed server's public IP address
- External Redis URL (optional - default empty) - File upload size
- AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) <br>Default: 5MB
- Specify external database address when using an external database
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/postgres`
- Specify external Redis URL when using external Redis
<br>Default: `Empty`
<br>`Default folder: /opt/plane/data/redis`
- Configure AWS S3 bucket
<br>Use only when you or your users want to use S3
<br>`Default folder: /opt/plane/data/minio`
1. Upgrade Plane using `plane-app --upgrade`. This will get the latest stable version of Plane files (docker-compose.yaml, .env, and docker images) 3. Version operators
1. Updating Plane App installer using `plane-app --update-installer` will update the `plane-app` utility. 1. `plane-app --upgrade` gets the latest stable version of `docker-compose.yaml`, `.env`, and Docker images
2. `plane-app --update-installer` updates the installer and the `plane-app` utility.
1. Uninstall Plane using `plane-app --uninstall`. This will uninstall the Plane application from the server and all docker containers but do not remove the data stored in Postgres, Redis, and Minio. 3. `plane-app --uninstall` uninstalls the Plane application and all Docker containers from the server but leaves the data stored in
Postgres, Redis, and Minio alone.
1. Plane App can be reinstalled using `plane-app --install`. 4. `plane-app --install` installs the Plane app again.
<ins>Application Data is stored in the mentioned folders</ins>:
1. DB Data: /opt/plane/data/postgres
1. Redis Data: /opt/plane/data/redis
1. Minio Data: /opt/plane/data/minio

View File

@ -58,6 +58,7 @@ export interface IGroupByKanBan {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
showEmptyGroup?: boolean; showEmptyGroup?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
} }
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => { const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
scrollableContainerRef, scrollableContainerRef,
isDragStarted, isDragStarted,
showEmptyGroup = true, showEmptyGroup = true,
subGroupIssueHeaderCount,
} = props; } = props;
const member = useMember(); const member = useMember();
@ -105,44 +107,57 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
if (!list) return null; if (!list) return null;
const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const groupList = showEmptyGroup ? list : groupWithIssues;
const visibilityGroupBy = (_list: IGroupByColumn) => {
if (sub_group_by) { if (sub_group_by) {
if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; const groupVisibility = {
return false; showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true;
}
return groupVisibility;
} else { } else {
if (kanbanFilters?.group_by.includes(_list.id)) return true; const groupVisibility = {
return false; showGroup: true,
showIssues: true,
};
if (!showEmptyGroup) {
if ((issueIds as TGroupedIssues)?.[_list.id]?.length > 0) groupVisibility.showGroup = true;
else groupVisibility.showGroup = false;
}
if (kanbanFilters?.group_by.includes(_list.id)) groupVisibility.showIssues = false;
return groupVisibility;
} }
}; };
const isGroupByCreatedBy = group_by === "created_by"; const isGroupByCreatedBy = group_by === "created_by";
return ( return (
<div className={`relative flex w-full gap-2 ${sub_group_by ? "h-full" : "h-full"}`}> <div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
{groupList && {list &&
groupList.length > 0 && list.length > 0 &&
groupList.map((_list: IGroupByColumn) => { list.map((subList: IGroupByColumn) => {
const groupByVisibilityToggle = visibilityGroupBy(_list); const groupByVisibilityToggle = visibilityGroupBy(subList);
if (groupByVisibilityToggle.showGroup === false) return <></>;
return ( return (
<div <div
key={_list.id} key={subList.id}
className={`group relative flex flex-shrink-0 flex-col ${groupByVisibilityToggle ? `` : `w-[350px]`}`} className={`relative flex flex-shrink-0 flex-col group ${
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
} `}
> >
{sub_group_by === null && ( {sub_group_by === null && (
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1"> <div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
<HeaderGroupByCard <HeaderGroupByCard
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
column_id={_list.id} column_id={subList.id}
icon={_list.icon} icon={subList.icon}
title={_list.name} title={subList.name}
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0} count={(issueIds as TGroupedIssues)?.[subList.id]?.length || 0}
issuePayload={_list.payload} issuePayload={subList.payload}
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy} disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
storeType={storeType} storeType={storeType}
addIssuesToView={addIssuesToView} addIssuesToView={addIssuesToView}
@ -152,9 +167,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
</div> </div>
)} )}
{!groupByVisibilityToggle && ( {groupByVisibilityToggle.showIssues && (
<KanbanGroup <KanbanGroup
groupId={_list.id} groupId={subList.id}
issuesMap={issuesMap} issuesMap={issuesMap}
issueIds={issueIds} issueIds={issueIds}
peekIssueId={peekIssue?.issueId ?? ""} peekIssueId={peekIssue?.issueId ?? ""}
@ -170,7 +185,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
viewId={viewId} viewId={viewId}
disableIssueCreation={disableIssueCreation} disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties} canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
/> />
@ -208,6 +222,7 @@ export interface IKanBan {
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
subGroupIssueHeaderCount?: (listId: string) => number;
} }
export const KanBan: React.FC<IKanBan> = observer((props) => { export const KanBan: React.FC<IKanBan> = observer((props) => {
@ -232,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef, scrollableContainerRef,
isDragStarted, isDragStarted,
showEmptyGroup, showEmptyGroup,
subGroupIssueHeaderCount,
} = props; } = props;
const issueKanBanView = useKanbanView(); const issueKanBanView = useKanbanView();
@ -259,6 +275,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
scrollableContainerRef={scrollableContainerRef} scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted} isDragStarted={isDragStarted}
showEmptyGroup={showEmptyGroup} showEmptyGroup={showEmptyGroup}
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
/> />
); );
}); });

View File

@ -36,7 +36,7 @@ interface IKanbanGroup {
viewId?: string; viewId?: string;
disableIssueCreation?: boolean; disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean; canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle: boolean; groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>; scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean; isDragStarted?: boolean;
} }

View File

@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader {
kanbanFilters: TIssueKanbanFilters; kanbanFilters: TIssueKanbanFilters;
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
storeType: KanbanStoreType; storeType: KanbanStoreType;
showEmptyGroup: boolean;
} }
const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => {
@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st
return headerCount; return headerCount;
}; };
const visibilitySubGroupByGroupCount = (
issueIds: TSubGroupedIssues,
_list: IGroupByColumn,
showEmptyGroup: boolean
): boolean => {
let subGroupHeaderVisibility = true;
if (showEmptyGroup) subGroupHeaderVisibility = true;
else {
if (getSubGroupHeaderIssuesCount(issueIds, _list.id) > 0) subGroupHeaderVisibility = true;
else subGroupHeaderVisibility = false;
}
return subGroupHeaderVisibility;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
issueIds, issueIds,
sub_group_by, sub_group_by,
@ -47,26 +64,37 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
list, list,
kanbanFilters, kanbanFilters,
handleKanbanFilters, handleKanbanFilters,
showEmptyGroup,
}) => ( }) => (
<div className="relative flex h-max min-h-full w-full items-center gap-2"> <div className="relative flex h-max min-h-full w-full items-center gap-2">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: IGroupByColumn) => ( list.map((_list: IGroupByColumn) => {
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col"> const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
<HeaderGroupByCard issueIds as TSubGroupedIssues,
sub_group_by={sub_group_by} _list,
group_by={group_by} showEmptyGroup
column_id={_list.id} );
icon={_list.icon}
title={_list.name} if (subGroupByVisibilityToggle === false) return <></>;
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters} return (
handleKanbanFilters={handleKanbanFilters} <div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
issuePayload={_list.payload} <HeaderGroupByCard
storeType={storeType} sub_group_by={sub_group_by}
/> group_by={group_by}
</div> column_id={_list.id}
))} icon={_list.icon}
title={_list.name}
count={getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, _list?.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
issuePayload={_list.payload}
storeType={storeType}
/>
</div>
);
})}
</div> </div>
); );
@ -127,53 +155,74 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
return issueCount; return issueCount;
}; };
const visibilitySubGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => {
const subGroupVisibility = {
showGroup: true,
showIssues: true,
};
if (showEmptyGroup) subGroupVisibility.showGroup = true;
else {
if (calculateIssueCount(_list.id) > 0) subGroupVisibility.showGroup = true;
else subGroupVisibility.showGroup = false;
}
if (kanbanFilters?.sub_group_by.includes(_list.id)) subGroupVisibility.showIssues = false;
return subGroupVisibility;
};
return ( return (
<div className="relative h-max min-h-full w-full"> <div className="relative h-max min-h-full w-full">
{list && {list &&
list.length > 0 && list.length > 0 &&
list.map((_list: any) => ( list.map((_list: any) => {
<div key={_list.id} className="flex flex-shrink-0 flex-col"> const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1"> if (subGroupByVisibilityToggle.showGroup === false) return <></>;
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2"> return (
<HeaderSubGroupByCard <div key={_list.id} className="flex flex-shrink-0 flex-col">
column_id={_list.id} <div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
icon={_list.Icon} <div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
title={_list.name || ""} <HeaderSubGroupByCard
count={calculateIssueCount(_list.id)} column_id={_list.id}
kanbanFilters={kanbanFilters} icon={_list.Icon}
handleKanbanFilters={handleKanbanFilters} title={_list.name || ""}
/> count={calculateIssueCount(_list.id)}
kanbanFilters={kanbanFilters}
handleKanbanFilters={handleKanbanFilters}
/>
</div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div> </div>
<div className="w-full border-b border-dashed border-custom-border-400" />
</div>
{!kanbanFilters?.sub_group_by.includes(_list.id) && ( {subGroupByVisibilityToggle.showIssues && (
<div className="relative"> <div className="relative">
<KanBan <KanBan
issuesMap={issuesMap} issuesMap={issuesMap}
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]} issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
displayProperties={displayProperties} displayProperties={displayProperties}
sub_group_by={sub_group_by} sub_group_by={sub_group_by}
group_by={group_by} group_by={group_by}
sub_group_id={_list.id} sub_group_id={_list.id}
updateIssue={updateIssue} storeType={storeType}
quickActions={quickActions} updateIssue={updateIssue}
kanbanFilters={kanbanFilters} quickActions={quickActions}
handleKanbanFilters={handleKanbanFilters} kanbanFilters={kanbanFilters}
showEmptyGroup={showEmptyGroup} handleKanbanFilters={handleKanbanFilters}
enableQuickIssueCreate={enableQuickIssueCreate} showEmptyGroup={showEmptyGroup}
canEditProperties={canEditProperties} enableQuickIssueCreate={enableQuickIssueCreate}
addIssuesToView={addIssuesToView} canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback} addIssuesToView={addIssuesToView}
viewId={viewId} quickAddCallback={quickAddCallback}
scrollableContainerRef={scrollableContainerRef} viewId={viewId}
isDragStarted={isDragStarted} scrollableContainerRef={scrollableContainerRef}
storeType={storeType} isDragStarted={isDragStarted}
/> subGroupIssueHeaderCount={(groupByListId: string) =>
</div> getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
)} }
</div> />
))} </div>
)}
</div>
);
})}
</div> </div>
); );
}); });
@ -267,6 +316,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
handleKanbanFilters={handleKanbanFilters} handleKanbanFilters={handleKanbanFilters}
list={groupByList} list={groupByList}
storeType={storeType} storeType={storeType}
showEmptyGroup={showEmptyGroup}
/> />
</div> </div>