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

This commit is contained in:
pablohashescobar 2024-03-15 13:11:35 +05:30
commit 7fdfe39bf0
5 changed files with 204 additions and 141 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
- 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.

View File

@ -178,7 +178,11 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="text-xs text-custom-text-300 flex-shrink-0">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div>
</div>
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end">
{currentCycle && (
<div
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
@ -192,11 +196,6 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
: `${currentCycle.label}`}
</div>
)}
</div>
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 overflow-hidden md:w-auto md:flex-shrink-0 md:justify-end">
<div className="text-xs text-custom-text-300">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div>
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>

View File

@ -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}
/>
);
});

View File

@ -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;
}

View File

@ -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>