forked from github/plane
Merge branch 'preview' of github.com:makeplane/plane into develop
This commit is contained in:
commit
80761d3507
@ -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 -
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Downloading Preview Release</summary>
|
||||
### 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
|
||||
|
||||
</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)
|
||||
|
||||
<ins>Basic Operations</ins>:
|
||||
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.
|
||||
|
||||
<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)
|
||||
- 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
|
||||
<br>Default: 80
|
||||
- Change your domain name
|
||||
<br>Default: Deployed server's public IP address
|
||||
- File upload size
|
||||
<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. 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`.
|
||||
|
||||
<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
|
||||
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.
|
||||
|
@ -58,6 +58,7 @@ export interface IGroupByKanBan {
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
showEmptyGroup?: boolean;
|
||||
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||
}
|
||||
|
||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
@ -83,6 +84,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
showEmptyGroup = true,
|
||||
subGroupIssueHeaderCount,
|
||||
} = props;
|
||||
|
||||
const member = useMember();
|
||||
@ -105,44 +107,57 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = 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 (
|
||||
<div className={`relative flex w-full gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||
{groupList &&
|
||||
groupList.length > 0 &&
|
||||
groupList.map((_list: IGroupByColumn) => {
|
||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
||||
<div className={`relative w-full flex gap-2 ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((subList: IGroupByColumn) => {
|
||||
const groupByVisibilityToggle = visibilityGroupBy(subList);
|
||||
|
||||
if (groupByVisibilityToggle.showGroup === false) return <></>;
|
||||
return (
|
||||
<div
|
||||
key={_list.id}
|
||||
className={`group relative flex flex-shrink-0 flex-col ${groupByVisibilityToggle ? `` : `w-[350px]`}`}
|
||||
key={subList.id}
|
||||
className={`relative flex flex-shrink-0 flex-col group ${
|
||||
groupByVisibilityToggle.showIssues ? `w-[350px]` : ``
|
||||
} `}
|
||||
>
|
||||
{sub_group_by === null && (
|
||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
||||
<HeaderGroupByCard
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
column_id={_list.id}
|
||||
icon={_list.icon}
|
||||
title={_list.name}
|
||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
||||
issuePayload={_list.payload}
|
||||
column_id={subList.id}
|
||||
icon={subList.icon}
|
||||
title={subList.name}
|
||||
count={(issueIds as TGroupedIssues)?.[subList.id]?.length || 0}
|
||||
issuePayload={subList.payload}
|
||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||
storeType={storeType}
|
||||
addIssuesToView={addIssuesToView}
|
||||
@ -152,9 +167,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!groupByVisibilityToggle && (
|
||||
{groupByVisibilityToggle.showIssues && (
|
||||
<KanbanGroup
|
||||
groupId={_list.id}
|
||||
groupId={subList.id}
|
||||
issuesMap={issuesMap}
|
||||
issueIds={issueIds}
|
||||
peekIssueId={peekIssue?.issueId ?? ""}
|
||||
@ -170,7 +185,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = 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<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
subGroupIssueHeaderCount?: (listId: string) => number;
|
||||
}
|
||||
|
||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
@ -232,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
scrollableContainerRef,
|
||||
isDragStarted,
|
||||
showEmptyGroup,
|
||||
subGroupIssueHeaderCount,
|
||||
} = props;
|
||||
|
||||
const issueKanBanView = useKanbanView();
|
||||
@ -259,6 +275,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
subGroupIssueHeaderCount={subGroupIssueHeaderCount}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ interface IKanbanGroup {
|
||||
viewId?: string;
|
||||
disableIssueCreation?: boolean;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
groupByVisibilityToggle: boolean;
|
||||
groupByVisibilityToggle?: boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isDragStarted?: boolean;
|
||||
}
|
||||
|
@ -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<ISubGroupSwimlaneHeader> = ({
|
||||
issueIds,
|
||||
sub_group_by,
|
||||
@ -47,26 +64,37 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||
list,
|
||||
kanbanFilters,
|
||||
handleKanbanFilters,
|
||||
showEmptyGroup,
|
||||
}) => (
|
||||
<div className="relative flex h-max min-h-full w-full items-center gap-2">
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: IGroupByColumn) => (
|
||||
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||
<HeaderGroupByCard
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
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>
|
||||
))}
|
||||
list.map((_list: IGroupByColumn) => {
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(
|
||||
issueIds as TSubGroupedIssues,
|
||||
_list,
|
||||
showEmptyGroup
|
||||
);
|
||||
|
||||
if (subGroupByVisibilityToggle === false) return <></>;
|
||||
|
||||
return (
|
||||
<div key={`${sub_group_by}_${_list.id}`} className="flex w-[350px] flex-shrink-0 flex-col">
|
||||
<HeaderGroupByCard
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
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>
|
||||
);
|
||||
|
||||
@ -127,53 +155,74 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = 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 (
|
||||
<div className="relative h-max min-h-full w-full">
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: any) => (
|
||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
|
||||
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
|
||||
<HeaderSubGroupByCard
|
||||
column_id={_list.id}
|
||||
icon={_list.Icon}
|
||||
title={_list.name || ""}
|
||||
count={calculateIssueCount(_list.id)}
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
/>
|
||||
list.map((_list: any) => {
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupBy(_list);
|
||||
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
||||
return (
|
||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||
<div className="sticky top-[50px] z-[1] flex w-full items-center bg-custom-background-90 py-1">
|
||||
<div className="sticky left-0 flex-shrink-0 bg-custom-background-90 pr-2">
|
||||
<HeaderSubGroupByCard
|
||||
column_id={_list.id}
|
||||
icon={_list.Icon}
|
||||
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 className="w-full border-b border-dashed border-custom-border-400" />
|
||||
</div>
|
||||
|
||||
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
|
||||
<div className="relative">
|
||||
<KanBan
|
||||
issuesMap={issuesMap}
|
||||
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
sub_group_id={_list.id}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
canEditProperties={canEditProperties}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
storeType={storeType}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{subGroupByVisibilityToggle.showIssues && (
|
||||
<div className="relative">
|
||||
<KanBan
|
||||
issuesMap={issuesMap}
|
||||
issueIds={(issueIds as TSubGroupedIssues)?.[_list.id]}
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
sub_group_id={_list.id}
|
||||
storeType={storeType}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
kanbanFilters={kanbanFilters}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
canEditProperties={canEditProperties}
|
||||
addIssuesToView={addIssuesToView}
|
||||
quickAddCallback={quickAddCallback}
|
||||
viewId={viewId}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
isDragStarted={isDragStarted}
|
||||
subGroupIssueHeaderCount={(groupByListId: string) =>
|
||||
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@ -267,6 +316,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
list={groupByList}
|
||||
storeType={storeType}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user