diff --git a/deploy/1-click/README.md b/deploy/1-click/README.md index 08bc35b28..9ed2323de 100644 --- a/deploy/1-click/README.md +++ b/deploy/1-click/README.md @@ -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 -- Supported CPU Architechture: AMD64 / ARM64 / x86_64 / aarch64 - -### Downloading Latest Stable Release +Run ↓ on any CLI. ``` curl -fsSL https://raw.githubusercontent.com/makeplane/plane/master/deploy/1-click/install.sh | sh - - ``` -
- Downloading Preview Release +### Download the Preview release + +`Preview` builds do not support ARM64, AArch64 CPU architectures + +Run ↓ on any CLI. ``` export BRANCH=preview - 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 - -
- --- - -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) -Basic Operations: +1. Basic operators -1. Start Server using `plane-app start` -1. Stop Server using `plane-app stop` -1. Restart Server using `plane-app restart` + 1. `plane-app start` starts the Plane server. + 2. `plane-app restart` restarts the Plane server. + 3. `plane-app stop` stops the Plane server. -Advanced Operations: +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) - - Domain Name (default is the local server public IP address) - - File Upload Size (default 5MB) - - External Postgres DB Url (optional - default empty) - - External Redis URL (optional - default empty) - - AWS S3 Bucket (optional - to be configured only in case the user wants to use an S3 Bucket) + - Change your proxy or listening port +
Default: 80 + - Change your domain name +
Default: Deployed server's public IP address + - File upload size +
Default: 5MB + - Specify external database address when using an external database +
Default: `Empty` +
`Default folder: /opt/plane/data/postgres` + - Specify external Redis URL when using external Redis +
Default: `Empty` +
`Default folder: /opt/plane/data/redis` + - Configure AWS S3 bucket +
Use only when you or your users want to use S3 +
`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. 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. - -1. Plane App can be reinstalled using `plane-app --install`. - -Application Data is stored in the mentioned folders: - -1. DB Data: /opt/plane/data/postgres -1. Redis Data: /opt/plane/data/redis -1. Minio Data: /opt/plane/data/minio + 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. + 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. + 4. `plane-app --install` installs the Plane app again. diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 394f5ef18..06f798d0f 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -58,6 +58,7 @@ export interface IGroupByKanBan { scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; showEmptyGroup?: boolean; + subGroupIssueHeaderCount?: (listId: string) => number; } const GroupByKanBan: React.FC = observer((props) => { @@ -83,6 +84,7 @@ const GroupByKanBan: React.FC = observer((props) => { scrollableContainerRef, isDragStarted, showEmptyGroup = true, + subGroupIssueHeaderCount, } = props; const member = useMember(); @@ -105,44 +107,57 @@ const GroupByKanBan: React.FC = observer((props) => { if (!list) return null; - const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); - - const groupList = showEmptyGroup ? list : groupWithIssues; - - const visibilityGroupBy = (_list: IGroupByColumn) => { + const visibilityGroupBy = (_list: IGroupByColumn): { showGroup: boolean; showIssues: boolean } => { if (sub_group_by) { - if (kanbanFilters?.sub_group_by.includes(_list.id)) return true; - return false; + const groupVisibility = { + showGroup: true, + showIssues: true, + }; + if (!showEmptyGroup) { + groupVisibility.showGroup = subGroupIssueHeaderCount ? subGroupIssueHeaderCount(_list.id) > 0 : true; + } + return groupVisibility; } else { - if (kanbanFilters?.group_by.includes(_list.id)) return true; - return false; + const groupVisibility = { + 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"; return ( -
- {groupList && - groupList.length > 0 && - groupList.map((_list: IGroupByColumn) => { - const groupByVisibilityToggle = visibilityGroupBy(_list); +
+ {list && + list.length > 0 && + list.map((subList: IGroupByColumn) => { + const groupByVisibilityToggle = visibilityGroupBy(subList); + if (groupByVisibilityToggle.showGroup === false) return <>; return (
{sub_group_by === null && (
= observer((props) => {
)} - {!groupByVisibilityToggle && ( + {groupByVisibilityToggle.showIssues && ( = observer((props) => { viewId={viewId} disableIssueCreation={disableIssueCreation} canEditProperties={canEditProperties} - groupByVisibilityToggle={groupByVisibilityToggle} scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} /> @@ -208,6 +222,7 @@ export interface IKanBan { canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; + subGroupIssueHeaderCount?: (listId: string) => number; } export const KanBan: React.FC = observer((props) => { @@ -232,6 +247,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef, isDragStarted, showEmptyGroup, + subGroupIssueHeaderCount, } = props; const issueKanBanView = useKanbanView(); @@ -259,6 +275,7 @@ export const KanBan: React.FC = observer((props) => { scrollableContainerRef={scrollableContainerRef} isDragStarted={isDragStarted} showEmptyGroup={showEmptyGroup} + subGroupIssueHeaderCount={subGroupIssueHeaderCount} /> ); }); diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 48e92feba..24f3b9532 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -36,7 +36,7 @@ interface IKanbanGroup { viewId?: string; disableIssueCreation?: boolean; canEditProperties: (projectId: string | undefined) => boolean; - groupByVisibilityToggle: boolean; + groupByVisibilityToggle?: boolean; scrollableContainerRef?: MutableRefObject; isDragStarted?: boolean; } diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 6b61e6661..644c254d6 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -29,6 +29,7 @@ interface ISubGroupSwimlaneHeader { kanbanFilters: TIssueKanbanFilters; handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void; storeType: KanbanStoreType; + showEmptyGroup: boolean; } const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: string) => { @@ -39,6 +40,22 @@ const getSubGroupHeaderIssuesCount = (issueIds: TSubGroupedIssues, groupById: st 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 = ({ issueIds, sub_group_by, @@ -47,26 +64,37 @@ const SubGroupSwimlaneHeader: React.FC = ({ list, kanbanFilters, handleKanbanFilters, + showEmptyGroup, }) => (
{list && list.length > 0 && - list.map((_list: IGroupByColumn) => ( -
- -
- ))} + list.map((_list: IGroupByColumn) => { + const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount( + issueIds as TSubGroupedIssues, + _list, + showEmptyGroup + ); + + if (subGroupByVisibilityToggle === false) return <>; + + return ( +
+ +
+ ); + })}
); @@ -127,53 +155,74 @@ const SubGroupSwimlane: React.FC = observer((props) => { 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 (
{list && list.length > 0 && - list.map((_list: any) => ( -
-
-
- + list.map((_list: any) => { + const subGroupByVisibilityToggle = visibilitySubGroupBy(_list); + if (subGroupByVisibilityToggle.showGroup === false) return <>; + return ( +
+
+
+ +
+
-
-
- {!kanbanFilters?.sub_group_by.includes(_list.id) && ( -
- -
- )} -
- ))} + {subGroupByVisibilityToggle.showIssues && ( +
+ + getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId) + } + /> +
+ )} +
+ ); + })}
); }); @@ -267,6 +316,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { handleKanbanFilters={handleKanbanFilters} list={groupByList} storeType={storeType} + showEmptyGroup={showEmptyGroup} />