시리즈 목록

윈도우(windows) WSL Ubuntu에 virtualenv 설치 및 vscode 연동
윈도우(windows) WSL Ubuntu에 pyenv 설치
윈도우(windows) WSL Ubuntu에 도커(Docker) 설치
윈도우(windows) WSL에 Ubuntu 20.04 LTS 추가 설치
윈도우(windows) WSL 명령어
윈도우(windows) WSL 설치 및 Ubuntu 구동

 

설치가능한 배포판 확인

PS C:\Users\gabriel> wsl --list --online
다음은 설치할 수 있는 유효한 배포판 목록입니다.
'wsl.exe --install <Distro>'를 사용하여 설치합니다.

NAME                                   FRIENDLY NAME
Ubuntu                                 Ubuntu
Debian                                 Debian GNU/Linux
kali-linux                             Kali Linux Rolling
Ubuntu-18.04                           Ubuntu 18.04 LTS
Ubuntu-20.04                           Ubuntu 20.04 LTS
Ubuntu-22.04                           Ubuntu 22.04 LTS
OracleLinux_7_9                        Oracle Linux 7.9
OracleLinux_8_7                        Oracle Linux 8.7
OracleLinux_9_1                        Oracle Linux 9.1
SUSE-Linux-Enterprise-Server-15-SP4    SUSE Linux Enterprise Server 15 SP4
openSUSE-Leap-15.4                     openSUSE Leap 15.4
openSUSE-Tumbleweed                    openSUSE Tumbleweed
PS C:\Users\gabriel>

 

특정 배포판 설치

wsl --install -d Ubuntu-20.04

 

설치된 Linux 확인

PS C:\Users\gabriel> wsl -l -v
  NAME            STATE           VERSION
* Ubuntu          Stopped         2
  Ubuntu-20.04    Stopped         1

 

WSL 2로 전환

PS C:\Users\gabriel> wsl --set-version Ubuntu-20.04 2
WSL 2와의 주요 차이점에 대한 자세한 내용은 https://aka.ms/wsl2를 참조하세요

변환이 진행 중입니다. 이 작업은 몇 분 정도 걸릴 수 있습니다.
작업을 완료했습니다.
PS C:\Users\gabriel> wsl -l -v
  NAME            STATE           VERSION
* Ubuntu          Stopped         2
  Ubuntu-20.04    Stopped         2
PS C:\Users\gabriel>

 

특정 배포를 기본으로 사용하고자 할경우

wsl --set-default <배포판이름>  or wsl -s <배포판이름>

PS C:\Users\gabriel> wsl --set-default Ubuntu-20.04
작업을 완료했습니다.
PS C:\Users\gabriel> wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-20.04    Stopped         2
  Ubuntu          Stopped         2
PS C:\Users\gabriel>

 

WSL 업데이트

PS C:\Users\gabriel> wsl --update
설치 중: Linux용 Windows 하위 시스템
Linux용 Windows 하위 시스템이(가) 설치되었습니다.
PS C:\Users\gabriel>

 

WSL 상태확인

PS C:\Users\gabriel> wsl --status
기본 배포: Ubuntu
기본 버전: 2
PS C:\Users\gabriel>

 

실행중인 Linux 배포판 Shutdown

PS C:\Users\gabriel> wsl --shutdown
PS C:\Users\gabriel>

 

Linux 등록취소/제거

wsl --unregister <배포판 이름>

PS C:\Users\gabriel> wsl --unregister Ubuntu-20.04
등록 취소 중입니다.
작업을 완료했습니다.
PS C:\Users\gabriel> wsl -l -v
  NAME      STATE           VERSION
* Ubuntu    Stopped         2
PS C:\Users\gabriel>

등록취소후 실행하고자 할때

삭제후 터미털(관리자모드)로 새로 실행후 배포목록을 확인하면 없어진것 확인가능

개발 환경구성에서는 Windows 10/11 PC에서 Linux 서브 운영체제로 Ubuntu를 사용해서 Docker 및 Python 개발환경을 구성하도록 합니다.

 

- 서브운영체제를 위한 WSL 설치

- WSL에 Ubuntu 구동

- Docker Desktop 대신 Ubuntu에 Docker 및 Docker Compose 설치

- Python 개발 및 가상환경을 위해 pyenv, virtualenv 설치 및 구성

- 등등..

 

시리즈 목록

윈도우(windows) WSL Ubuntu에 virtualenv 설치 및 vscode 연동
윈도우(windows) WSL Ubuntu에 pyenv 설치
윈도우(windows) WSL Ubuntu에 도커(Docker) 설치
윈도우(windows) WSL에 Ubuntu 20.04 LTS 추가 설치
윈도우(windows) WSL 명령어
윈도우(windows) WSL 설치 및 Ubuntu 구동

 

내 윈도우 PC의 버전을 확인

 

 

windows terminal 설치

 

Windows 11에는 기본으로 설치되어 있습니다.(설치전이면 "다운로드", 설치되어 있어서 "열기")

다운로드 완료

 

관리자 권한으로 Terminal 열기

WSL 설치

 

1. Windows 10에서 설치시 "Linux용 Windows 하위 시스템", "가상 머신 플랫폼" 체크해서 활성화

 

첫 번째 방법 - 윈도우 기능 켜기/끄기

 

appwiz.cpl 입력 > Windows 기능 켜기/끄기 선택 >  "Linux용 Windows 하위 시스템", "가상 머신 플랫폼" 체크해서 활성화

 

두 번째 방법 - 명령행으로 실행 (해당 옵션으로 설치했음)

 

DISM(배포 이미지 서비스 및 관리) 명령어로 Microsoft-Windows-Subsystem-Linux 기능을 활성화

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

PS C:\Users\gabriel> dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

배포 이미지 서비스 및 관리 도구
버전: 10.0.19041.844

이미지 버전: 10.0.19045.2728

기능을 사용하도록 설정하는 중
[==========================100.0%==========================]
작업을 완료했습니다.
PS C:\Users\gabriel>

dism 명령어로 VirtualMachinePlatform 기능을 활성화

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

PS C:\Users\gabriel> dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

배포 이미지 서비스 및 관리 도구
버전: 10.0.19041.844

이미지 버전: 10.0.19045.2728

기능을 사용하도록 설정하는 중
[==========================100.0%==========================]
작업을 완료했습니다.
PS C:\Users\gabriel>

윈도우 재부팅

 

x64 머신용 최신 WSL2 Linux 커널 업데이트 패키지 (필수)

https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi

 

 

기본적으로 사용할 WSL 버전을 2로 변경

wsl --set-default-version 2

PS C:\Users\gabriel> wsl --set-default-version 2
WSL 2와의 주요 차이점에 대한 자세한 내용은 https://aka.ms/wsl2를 참조하세요
작업을 완료했습니다.
PS C:\Users\gabriel>

 

wsl 버전확인

PS C:\Users\gabriel> wsl --update
업데이트 확인 중입니다.
Linux용 Windows 하위 시스템 최신 버전이 이미 설치되어 있습니다.
PS C:\Users\gabriel> wsl --version
WSL 버전: 1.2.5.0
커널 버전: 5.15.90.1
WSLg 버전: 1.0.51
MSRDC 버전: 1.2.3770
Direct3D 버전: 1.608.2-61064218
DXCore 버전: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp
Windows 버전: 10.0.19045.2728
PS C:\Users\gabriel>

 

Ubuntu 최신 배포판 설치

Windows 10 버전 2004 이상(빌드 19041 이상) 또는 Windows 11 인경우 아래 명령으로 기본 Ubuntu 최신 (22.02.4) 설치가능

wsl --install

만일 해당 명령으로 설치가 안될경우 아래 두가지 방법으로 수동으로 설치합니다.

첫번째 Microsoft Store에서 ubuntu 설치 (해당 옵션으로 설치했음)

 

 

"열기"를 누르면 자동으로 설치 prompt가 실행되며, 사용자 id, passwd를 입력합니다.

설치가 완료되면 exit

 

관리자 권한으로 터미널 실행

두번째, wsl install을 이용해서 Ubuntu 설치

설치가능한 목록 확인

PS C:\Users\gabriel> wsl --list --online
다음은 설치할 수 있는 유효한 배포 목록입니다.
'wsl --install -d <배포>'를 사용하여 설치하세요.

NAME                                   FRIENDLY NAME
Ubuntu                                 Ubuntu
Debian                                 Debian GNU/Linux
kali-linux                             Kali Linux Rolling
Ubuntu-18.04                           Ubuntu 18.04 LTS
Ubuntu-20.04                           Ubuntu 20.04 LTS
Ubuntu-22.04                           Ubuntu 22.04 LTS
OracleLinux_7_9                        Oracle Linux 7.9
OracleLinux_8_7                        Oracle Linux 8.7
OracleLinux_9_1                        Oracle Linux 9.1
SUSE-Linux-Enterprise-Server-15-SP4    SUSE Linux Enterprise Server 15 SP4
openSUSE-Leap-15.4                     openSUSE Leap 15.4
openSUSE-Tumbleweed                    openSUSE Tumbleweed
PS C:\Users\gabriel>

 

wsl --install -d Ubuntu

PS C:\Users\gabriel> wsl --install -d Ubuntu

ubuntu 설치 prompt가 자동으로 실행, 이때 명시적으로 Ubuntu 버전을 지정하지 않으면 "Ubuntu 22.04 LTS" 가 설치됨.

 

아이디 패스워드 입력

설치된 ubuntu 확인

 

앞단계 WSL 설치후에 "wsl --set-default-version 2" 로 버전을 지정해준후 ubuntu를 설치했기 때문에 VERSION 2로 설치되었지만, 만일 VERSION이 1 이라면 아래 명령으로 2로 바꿔 주도록 합니다.

wsl --set-version Ubuntu 2

 

새로 터미널을 열고 선택박스에서  Ubuntu 를 선택하여 실행 합니다.

설치된 ubuntu의 버전 확인 : cat /etc/lsb-release

설치후 Ubuntu 업데이트 및 업그레이드 

gabriel@NB-15052600:~$ sudo apt update && sudo apt upgrade
[sudo] password for gabriel: <패스워드 입력>
Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease
Get:2 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB]
Get:3 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [119 kB]
Get:4 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages [363 kB]
Get:5 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [108 kB]
Get:6 http://security.ubuntu.com/ubuntu jammy-security/main Translation-en [108 kB]
Get:7 http://security.ubuntu.com/ubuntu jammy-security/main amd64 c-n-f Metadata [9732 B]
Get:8 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages [225 kB]
Get:9 http://archive.ubuntu.com/ubuntu jammy/universe amd64 Packages [14.1 MB]

생략....

Preparing to unpack .../libkrb5-3_1.19.2-2ubuntu0.2_amd64.deb ...
Unpacking libkrb5-3:amd64 (1.19.2-2ubuntu0.2) over (1.19.2-2ubuntu0.1) ...
Setting up libkrb5-3:amd64 (1.19.2-2ubuntu0.2) ...
(Reading database ... 24137 files and directories currently installed.)
Preparing to unpack .../libgssapi-krb5-2_1.19.2-2ubuntu0.2_amd64.deb ...
Unpacking libgssapi-krb5-2:amd64 (1.19.2-2ubuntu0.2) over (1.19.2-2ubuntu0.1) ...
Setting up libgssapi-krb5-2:amd64 (1.19.2-2ubuntu0.2) ...
(Reading database ... 24137 files and directories currently installed.)
Preparing to unpack .../distro-info-data_0.52ubuntu0.4_all.deb ...
Unpacking distro-info-data (0.52ubuntu0.4) over (0.52ubuntu0.3) ...
Setting up distro-info-data (0.52ubuntu0.4) ...
Processing triggers for libc-bin (2.35-0ubuntu3.1) ...
gabriel@NB-15052600:~$

 

Ubuntu 22.04.2가 최신으로 설치됨

 

 

 

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py    
2 demo_board > demo_board.py    
3 demo_board > demo_state.py logincheck 함수 및 로그인 체크 시간 var 추가  
4 demo_board > demo_auth.py    
5 demo_board > demo_servers.py pc.cond 에 logincheck var로 로그인상태 체크 수정
6 demo_board > demo_helpers.py    

 

로그인 후 타임아웃시간을 지정해서 사용자가 일정시간동안 액션이 없다면 로그아웃시키는 기능을 추가합니다

별도의 세션을 체크하는 방법을 찾지 못해서 State에 login check var를 추가하고 로그인할때, 매뉴클릭시 각 화면의 권한 체크하는 pc.cond 로 login check var를 호출하도록 구현하도록 하겠습니다.

 

demo_state.py


날짜 변환 로컬 함수를 먼저 정의합니다.

from datetime import date, datetime, timezone, timedelta

# logincheck에서 사용할 함수 현재 시간을 리턴
def get_ymd_hms():
	# 클라우드 서버가 싱가폴 데이터 센터에 있기때문에 UTC=8로 한국시간과 맞춘다.
    servertime = timezone(timedelta(hours=8)) 
    now = datetime.now(servertime) 
    now_ymd = now.strftime('%Y%m%d')
    now_ymd_hms = now.strftime('%Y-%m-%d %H:%M:%S')
    #print("now_ymd_hms : {}".format(now_ymd_hms))
    
    return now_ymd_hms

# 문자열의 시간을 시간타입으로 변환
def get_strptime(ymd_hms):
    datetime_format = '%Y-%m-%d %H:%M:%S'

    datetime_ymd_hms = datetime.strptime(ymd_hms, datetime_format)
    
    return datetime_ymd_hms

 

logincheck var 추가

class State(pc.State):
    """The base state for the app."""
    
    username: str
    logged_in: bool = False # 로그인 상태 True, False
    authrole: str
    logged_ymd_hms:str #로그인 및 마지막 액션 시간을 저장

    @pc.var
    def logincheck(self) -> bool:
        """Logged in check a user."""
        
        # 현재 시간을 '%Y-%m-%d %H:%M:%S' 포켓으로 가져오기
        now_ymd_hms = get_ymd_hms()
        
        # 현재 로그인 상태이면
        if self.logged_in:
            
            # 로그인 상태이고, 로그인 시간이 기록되어 있으면
            if len(self.logged_ymd_hms) > 0:

				# 이전 로그인 또는 액션의 시간을 조회
                before_time = get_strptime(self.logged_ymd_hms)
                now_time = get_strptime(now_ymd_hms)

				# 현재 시간과 이전 시간의 차이를 계산
                cul_time = now_time - before_time
                
                # 차이가 지정된 시간보다 크다면 로그인 상태 False로
                if cul_time.seconds > 3600: #600 : 10 Minutes
                    self.logged_in = False
                    self.logged_ymd_hms = ""
                    return self.logged_in

            # 지정된 시간안에 있으면, 현재 시간을 logged_ymd_hms에 저장
            self.logged_ymd_hms = now_ymd_hms
        
        return self.logged_in

 

demo_servers.py


demo_servers.py 의 servers 함수에서 pc.cond에서 로그인 상태 체크를 logged_in 에서 logincheck var 로 변경한다.

 

def servers():
    """The Servers page."""
    return pc.center(
        pc.vstack(
            navbar(State, ServerState.app_name),
            pc.cond(
                State.logincheck,
                pc.box(
                    pc.vstack(
                        searchbar(ServerState),
                        progress_modal(ServerState),
                        render_table(ServerState),
                        #render_datatable(ServerState),
                        footer_bottom(),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="10em",
            width="100%",
        ),
    )

 

 

추가로 로그인후 액션없이 오래 떠있던 웹페이지에서 검색 버튼 클릭시 log in 상태를 체크할수 있도록 방어코드를 추가합니다.

(로그아웃 상태로 CircularProgress가 작동되지 않도록)

 

demo_servers.py

class ServerState(State):
    """The state for the Server page."""
    print("──────────────────── [ ServerState(State) ] ─────────────────────")
    app_name: str = "Server & Apps"
    
    
    생략 ...
    
    # 1. Progress 를 보여줄지 말지 결정할 Toggle 이벤트
    prog_show: bool = False
    def prog_show_change(self):
        """Toggle the Update Progress modal."""
        
        # 우선 로그인 상태를 체크해서 CircularProgress 모달이 뜨지 않도록 합니다.
        if self.logged_in == False:
            return
            
        self.prog_show = not (self.prog_show)
        
    생략 ...
        
    def get_data_list(self):
        #print("\n[ServerState] get_data_list")
        pd_result_list: List[Dict] = get_server_data(self.select1_id, self.select2_id)
        self.table_columns = list(pd_result_list.columns)
        self.table_data = pd_result_list.values.tolist()
        #print("[ServerState] get_data_list pd_result_list len : {}\n".format(len(pd_result_list)))

        # 2. 기존 "검색" 버튼 클릭시 데이터 조회하는 Class 함수에 
        # Progress 컨포넌트 상태 변경 이벤트를 호출
        return self.prog_show_change()

 

아래 그림처럼 login 체크 시간을 보이도록 설정할수도 있습니다. (Optional demo_helpers.py 수정)

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py    
2 demo_board > demo_board.py    
3 demo_board > demo_state.py    
4 demo_board > demo_auth.py    
5 demo_board > demo_servers.py 조회조건 선택후 "검색" 시 Progress 보여주기 수정
6 demo_board > demo_helpers.py    

 

기존 검색 버튼의 on_click 이벤트에 다중 이벤트로 Progress 를 붙여서 데이터 조회중이라는 상태를 표시하도록 한다.

 

필요한 컴포넌트 

Modal (https://pynecone.io/docs/library/overlay/modal)

 

1. Modal 컴포넌트를 생성

여기서는 Circlar Progress를 사용했습니다. 새로 함수 추가

def progress_modal(ServerState):
    """Display for an progress circle."""
    return pc.modal(
        pc.modal_overlay(
            pc.modal_content(
                pc.modal_header("Your service request is being processed."),
                pc.modal_body(
                    pc.center(
                        pc.circular_progress(is_indeterminate=True),
                    ),
                ),
                align="center",
            ),
        ),
        is_open=ServerState.prog_show,
    )

 

2. 생성한 Modal을 메인 UI 함수에 추가

progress_modal(ServerState)를 추가합니다.

def servers():
    """The Servers page."""
    return pc.center(
        pc.vstack(
            navbar(State, ServerState.app_name),
            pc.cond(
                State.logged_in,
                pc.box(
                    pc.vstack(
                        searchbar(ServerState),
                        progress_modal(ServerState),
                        render_table(ServerState),
                        #render_datatable(ServerState),
                        footer_bottom(),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="10em",
            width="100%",
        ),
    )

 

 

3. Modal 컴포넌트 이벤트 함수 등록

1번, 2번 코드를 기존 코드에 추가합니다.

class ServerState(State):
    """The state for the Server page."""
    print("──────────────────── [ ServerState(State) ] ─────────────────────")
    app_name: str = "Server & Apps"
    
    
    생략 ...
    
    # 1. Progress 를 보여줄지 말지 결정할 Toggle 이벤트
    prog_show: bool = False
    def prog_show_change(self):
        """Toggle the Update Progress modal."""
        self.prog_show = not (self.prog_show)
        
    생략 ...
        
    def get_data_list(self):
        #print("\n[ServerState] get_data_list")
        pd_result_list: List[Dict] = get_server_data(self.select1_id, self.select2_id)
        self.table_columns = list(pd_result_list.columns)
        self.table_data = pd_result_list.values.tolist()
        #print("[ServerState] get_data_list pd_result_list len : {}\n".format(len(pd_result_list)))

        # 2. 기존 "검색" 버튼 클릭시 데이터 조회하는 Class 함수에 
        # Progress 컨포넌트 상태 변경 이벤트를 호출
        return self.prog_show_change()

 

4. "검색" 버튼의 on_cllick 이벤트에 Progress를 띄우도록 다중 이벤트 등록

on_click=[ServerState.prog_show_change,ServerState.get_data_list]

먼저 prog_show_change 이벤트로 Progress Modal을 띄우고 get_data_list로 데이터 조회를 합니다.

def searchbar(ServerState):
    """The searchbar."""
    return pc.box(
    
    생략 ...
    
                pc.button(
                    "검색", #Search
                    bg="navy", #gray
                    color="white", 
                    size="md",
                    width="150px",
                    #on_click=ServerState.get_data_list,
                    on_click=[ServerState.prog_show_change,ServerState.get_data_list],
                ),

 

ServerState Class 함수의 get_data_list 에 리턴으로 상태변경 Toggle 함수를 호출 하도록 했기때문에 데이터 조회가 끝나면 함수가 호출되어 Progress Modal이 닫기게 됩니다.

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py    
2 demo_board > demo_board.py    
3 demo_board > demo_state.py    
4 demo_board > demo_auth.py    
5 demo_board > demo_servers.py 조회조건 선택후 "검색" 시 Progress 보여주기 수정
6 demo_board > demo_helpers.py    

 

데이터 조회시 시간이 오래 걸리게 되면 화면에서 사용자가 진행되고있는지 상태를 알수있도록, 기존 검색 버튼의 on_click 이벤트에 다중 이벤트로 Progress 를 붙여서 데이터 조회중이라는 상태를 표시하도록 한다.

 

 

필요한 컴포넌트 

Modal (https://pynecone.io/docs/library/overlay/modal)

 

1. Modal 컴포넌트를 생성

여기서는 Circlar Progress를 사용했습니다. 새로 함수 추가

def progress_modal(ServerState):
    """Display for an progress circle."""
    return pc.modal(
        pc.modal_overlay(
            pc.modal_content(
                pc.modal_header("Your service request is being processed."),
                pc.modal_body(
                    pc.center(
                        pc.circular_progress(is_indeterminate=True),
                    ),
                ),
                align="center",
            ),
        ),
        is_open=ServerState.prog_show,
    )

 

2. 생성한 Modal을 메인 UI 함수에 추가

progress_modal(ServerState)를 추가합니다.

def servers():
    """The Servers page."""
    return pc.center(
        pc.vstack(
            navbar(State, ServerState.app_name),
            pc.cond(
                State.logged_in,
                pc.box(
                    pc.vstack(
                        searchbar(ServerState),
                        progress_modal(ServerState),
                        render_table(ServerState),
                        #render_datatable(ServerState),
                        footer_bottom(),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="10em",
            width="100%",
        ),
    )

 

 

3. Modal 컴포넌트 이벤트 함수 등록

1번, 2번 코드를 기존 코드에 추가합니다.

class ServerState(State):
    """The state for the Server page."""
    print("──────────────────── [ ServerState(State) ] ─────────────────────")
    app_name: str = "Server & Apps"
    
    
    생략 ...
    
    # 1. Progress 를 보여줄지 말지 결정할 Toggle 이벤트
    prog_show: bool = False
    def prog_show_change(self):
        """Toggle the Update Progress modal."""
        self.prog_show = not (self.prog_show)
        
    생략 ...
        
    def get_data_list(self):
        #print("\n[ServerState] get_data_list")
        pd_result_list: List[Dict] = get_server_data(self.select1_id, self.select2_id)
        self.table_columns = list(pd_result_list.columns)
        self.table_data = pd_result_list.values.tolist()
        #print("[ServerState] get_data_list pd_result_list len : {}\n".format(len(pd_result_list)))

        # 2. 기존 "검색" 버튼 클릭시 데이터 조회하는 Class 함수에 
        # Progress 컨포넌트 상태 변경 이벤트를 호출
        return self.prog_show_change()

 

4. "검색" 버튼의 on_cllick 이벤트에 Progress를 띄우도록 다중 이벤트 등록

on_click=[ServerState.prog_show_change,ServerState.get_data_list]

먼저 prog_show_change 이벤트로 Progress Modal을 띄우고 get_data_list로 데이터 조회를 합니다.

def searchbar(ServerState):
    """The searchbar."""
    return pc.box(
    
    생략 ...
    
                pc.button(
                    "검색", #Search
                    bg="navy", #gray
                    color="white", 
                    size="md",
                    width="150px",
                    #on_click=ServerState.get_data_list,
                    on_click=[ServerState.prog_show_change,ServerState.get_data_list],
                ),

 

ServerState Class 함수의 get_data_list 에 리턴으로 상태변경 Toggle 함수를 호출 하도록 했기때문에 데이터 조회가 끝나면 함수가 호출되어 Progress Modal이 닫기게 됩니다.

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py    
2 demo_board > demo_board.py    
3 demo_board > demo_state.py    
4 demo_board > demo_auth.py    
5 demo_board > demo_servers.py 다중 Select 컴포넌트 추가 및 Dummy 데이터 수정
6 demo_board > demo_helpers.py    

 

서버 모니터링 demo_servers.py 에서 선택되는 Select 컴포넌트의 값에 따라서 여러 종류의 서버 유형의 결과를 조회하기위해서

Table 상단에 다중 Select 를 추가합니다.

 

수정전

 

수정후

 

Select 항목 선택후 "검색" 결과

수정된 내용

 

SeverState에서 사용할 함수들..

# Select 컴포넌트 2개에 표시될 데이터 List[Dict] Dummy 를 Dataframe으로 리턴
def get_select_data():
    result_list =  [
        {'service_id': 'app_mng', 'parent_id': 'servers', 'service_nm': '인프라 서버 관리'}, 
        {'service_id': 'app_web', 'parent_id': 'app_mng', 'service_nm': 'WEB 서버 관리'},
        {'service_id': 'app_was', 'parent_id': 'app_mng', 'service_nm': 'WAS 서버 관리'},
        {'service_id': 'app_rds', 'parent_id': 'app_mng', 'service_nm': 'DB 서버 관리'}
    ]
    result_list = pd.DataFrame(result_list)
    return result_list

# Dummy 를 Dataframe에서 상위 즉 parent_id가 동일한 항목들을 List로 리턴
def get_select_options(select_list, parent_id):
    return select_list[select_list['parent_id']==parent_id]['service_nm'].values.tolist()

# Dummy 를 Dataframe에서 선택된 service_name 으로 해당 Dict의 service_id를 찾아서 리턴
def get_select_id(select_list, select1_id, option):
    selected_parent = select_list[select_list['parent_id']==select1_id]
    select_selected=selected_parent[selected_parent['service_nm']==option]

    select_id=""
    if len(select_selected) > 0:
        select_id=select_selected['service_id'].values[0] 

    return select_id

 

class ServerState(State):

class ServerState(State):
    """The Substate of base state for the server page."""
    print("──────────────────── [ ServerState(State) ] ────────────────────")
    app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명

    # (추가됨)
    # select 컴포넌트에서 사용할 Dummy 데이타
    select_list = get_select_data() 

    # 화면 로드시에 첫번째 Select 컴포넌트에 값을 세팅
    select1_options: list[str] = get_select_options(select_list, "servers")
    # 두번째 Select는 빈값으로 두고 첫번째 Select가 선택될때 이벤트를 받아서 항목을 추가
    select2_options: list[str] = []

    # 현재 선택된 Select 항목의 값들을 저장하기위한 vars
    select1_id: str = "" 
    select2_id: str = "" 
    
    select1_value: str = "" 
    select2_value: str = "" 
    
    ...(생략)

    # (추가됨) Select 컴포넌트에서 on_change 이벤트가 발생할때 호출
    def select_change(self, level, value):
    
    	# 선택항목이 select1 이면, 
        if(level == "select1"):
            self.select1_id="" 
            self.select1_value=""
            self.select2_id=""
            self.select2_value="" 
            self.select2_options=[]
            
            # 선택된 Option service_nm 값으로 해당 select_id를 찾아온다.
            select_id = get_select_id(self.select_list, "servers", value)
            
            self.select1_id=select_id
            self.select1_value=value
            # 앞서 찾은 select1의 id 값으로 select2 컴포넌트를위한 옵션 List를 조회/등록
            self.select2_options=get_select_options(self.select_list, select_id)

        elif(level == "select2"):
        	# Select2 가 선택됐을때는 선택된 service_nm에 대한 service_id 를 찾아 var에 저장
            select_id = get_select_id(self.select_list, self.select1_id, value)
            self.select2_id=select_id
            self.select2_value=value
        else:
            return

    # (추가됨) select_change에서 선택된 값들을 해서 "검색" 버튼 클릭시 데이터를 조회
    def get_data_list(self):
        pd_result_list: List[Dict] = get_data(self.select2_id)
        self.table_columns = list(pd_result_list.columns)
        self.table_data = pd_result_list.values.tolist()

 

다중 Select 컴포넌트 그리기

def search_select(ServerState):
    """The Multi Select combo."""
    return pc.box(
        pc.hstack(
            pc.hstack(
                pc.select(
                    ServerState.select1_options,
                    placeholder="Select an Option.",
                    on_change=lambda value: ServerState.select_change('select1', value),
                    #is_disabled=True,
                    border_style="solid",
                    border_width="1px",
                    border_color="#43464B",
                    value=ServerState.select1_value,
                ),
                pc.select(
                    ServerState.select2_options,
                    placeholder="Select an Option.",
                    on_change=lambda value: ServerState.select_change('select2', value),
                    #is_disabled=True,
                    border_style="solid",
                    border_width="1px",
                    border_color="#43464B",
                    value=ServerState.select2_value,
                ),
                pc.button(
                    "검색", #Search
                    bg="navy", #gray
                    color="white", 
                    size="md",
                    width="150px",
                    on_click=ServerState.get_data_list,
                ),
                border_width="0px",
            ),
            border_width="0px",
        ),
        #position="fixed",
        #width="40%",
        #top="105px",
        z_index="500",
    )

 

마지막으로 "/server" 함수에 search_select 추가

def servers():
    return pc.box(
        pc.vstack(
            navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가 
            pc.cond(
                State.logged_in,
                pc.box(
                        #pc.heading("서버 모니터링", font_size="2em"),
                        #pc.heading("서버 모니터링"),
                        search_select(ServerState), # search_select 추가
                        render_table(ServerState),
                        #pc.divider(),
                        #render_datatable(ServerState),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/login",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="5.5em",
            width="100%",
        ),
    )

 

전체소스

import pynecone as pc

import pandas as pd
from typing import List, Dict

from .demo_state import State # Substate of Base State
from .demo_helpers import navbar

def get_select_data():
    result_list =  [
        {'service_id': 'app_mng', 'parent_id': 'servers', 'service_nm': '인프라 서버 관리'}, 
        {'service_id': 'app_web', 'parent_id': 'app_mng', 'service_nm': 'WEB 서버 관리'},
        {'service_id': 'app_was', 'parent_id': 'app_mng', 'service_nm': 'WAS 서버 관리'},
        {'service_id': 'app_rds', 'parent_id': 'app_mng', 'service_nm': 'DB 서버 관리'}
    ]
    result_list = pd.DataFrame(result_list)
    return result_list

def get_select_options(select_list, parent_id):
    return select_list[select_list['parent_id']==parent_id]['service_nm'].values.tolist()

def get_select_id(select_list, select1_id, option):
    selected_parent = select_list[select_list['parent_id']==select1_id]
    select_selected=selected_parent[selected_parent['service_nm']==option]

    select_id=""
    if len(select_selected) > 0:
        select_id=select_selected['service_id'].values[0] 

    return select_id

def get_data(server_type):
    # 샘플 데이터 List[Dict]
    if server_type == "" or server_type == None:
        result_list = [{'Result': 'No data found (검색 조건을 선택한 후 조회 버튼을 클릭하세요)'}]
    else:
        result_list = [
            {"Server Type": server_type, "Server Name": "alpha", "Total Disk": "10G", "Used Disk": "5G", "Available Disk": "5G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
            {"Server Type": server_type, "Server Name": "brovo", "Total Disk": "10G", "Used Disk": "5G", "Available Disk": "5G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
            {"Server Type": server_type, "Server Name": "charlie", "Total Disk": "10G", "Used Disk": "5G", "Available Disk": "5G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
            {"Server Type": server_type, "Server Name": "delta", "Total Disk": "10G", "Used Disk": "5G", "Available Disk": "5G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
            {"Server Type": server_type, "Server Name": "echo", "Total Disk": "10G", "Used Disk": "5G", "Available Disk": "5G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        ]

    pd_result_list = pd.DataFrame(result_list)
    pd_result_list=pd_result_list.fillna('')   # NaN값을 0 or ''로 치환한다.
    pd_result_list=pd_result_list.astype(str)  # 전체 Field를 str로 변환한다.

    return pd_result_list


class ServerState(State):
    """The Substate of base state for the server page."""
    print("──────────────────── [ ServerState(State) ] ────────────────────")
    app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명

    select_list = get_select_data()

    select1_options: list[str] = get_select_options(select_list, "servers")
    select2_options: list[str] = []

    select1_id: str = "" 
    select2_id: str = "" 
    
    select1_value: str = "" 
    select2_value: str = "" 


    pd_result_list: List[Dict] = get_data("")
    #print(pd_result_list)

    # table_columns : Dict에서 Columns 정보를 가져와서 Table의 thead를 구성
    # table_data : Dataframe에서 value만 List로 만들어서 Table의 tbody를 구성
    # table_name : Table의 table_caption 

    table_columns = list(pd_result_list.columns)
    table_data: List[List[str]] = pd_result_list.values.tolist()
    table_name: str = "WEB 서버 상태 확인"

    box_align: str = ""

    def select_change(self, level, value):

        if(level == "select1"):
            self.select1_id="" 
            self.select1_value=""
            self.select2_id=""
            self.select2_value="" 
            self.select2_options=[]
            
            select_id = get_select_id(self.select_list, "servers", value)
            
            self.select1_id=select_id
            self.select1_value=value
            self.select2_options=get_select_options(self.select_list, select_id)

        elif(level == "select2"):
            select_id = get_select_id(self.select_list, self.select1_id, value)
            self.select2_id=select_id
            self.select2_value=value
        else:
            return

    def get_data_list(self):
        pd_result_list: List[Dict] = get_data(self.select2_id)
        self.table_columns = list(pd_result_list.columns)
        self.table_data = pd_result_list.values.tolist()


def servers():
    return pc.box(
        pc.vstack(
            navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가 
            pc.cond(
                State.logged_in,
                pc.box(
                    pc.vstack(
                        search_select(ServerState),
                        render_table(ServerState),
                        #pc.divider(),
                        #render_datatable(ServerState),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/login",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="5.5em",
            width="100%",
        ),
    )

def search_select(ServerState):
    """The Multi Select combo."""
    return pc.box(
        pc.hstack(
            pc.hstack(
                pc.select(
                    ServerState.select1_options,
                    placeholder="Select an Option.",
                    on_change=lambda value: ServerState.select_change('select1', value),
                    #is_disabled=True,
                    border_style="solid",
                    border_width="1px",
                    border_color="#43464B",
                    value=ServerState.select1_value,
                ),
                pc.select(
                    ServerState.select2_options,
                    placeholder="Select an Option.",
                    on_change=lambda value: ServerState.select_change('select2', value),
                    #is_disabled=True,
                    border_style="solid",
                    border_width="1px",
                    border_color="#43464B",
                    value=ServerState.select2_value,
                ),
                pc.button(
                    "검색", #Search
                    bg="navy", #gray
                    color="white", 
                    size="md",
                    width="150px",
                    on_click=ServerState.get_data_list,
                ),
                border_width="0px",
            ),
            border_width="0px",
        ),
        #position="fixed",
        #width="40%",
        #top="105px",
        z_index="500",
    )

def render_tbody_tr(tr_data, index):
    return pc.tr(
        pc.td(index + 1),
        pc.foreach(tr_data, lambda data: pc.td(data)),
    )

def render_table(ServerState):
    return pc.box(
        pc.vstack(
            pc.table(
                pc.table_caption(ServerState.table_name),
                pc.thead(
                    pc.tr(
                        pc.td("#"),
                        pc.foreach(ServerState.table_columns, lambda data: pc.td(data)),
                    ),
                    bg="navy",
                    color="white",
                ),
                pc.tbody(
                    pc.foreach(ServerState.table_data, lambda data, i: render_tbody_tr(data, i)),
                ),
            ),
            border_width="1px",
            overflow="auto",
        ),
        padding_top="1em",
        width="98%",
    )

def render_datatable(ServerState):
    return pc.box(
        pc.vstack(
            pc.data_table(
                data=ServerState.pd_result_list,
                pagination=True,
                search=True,
                sort=False, #True, False
                resizable=True,
                border_color="#43464B",
            ),
            width="100%",
            #align_items="start",
            #align_items="left",
            align_items=ServerState.box_align,
            #padding_x="15%",
            overflow="auto",
            #overflow_x="scroll",
            #overflow_y="scroll",
        ),
        border_width="0px",
        border_color="#43464B",
        width="98%",
    )

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py    
2 demo_board > demo_board.py    
3 demo_board > demo_state.py    
4 demo_board > demo_auth.py    
5 demo_board > demo_servers.py List[Dict] 데이터로 Table, DataTable 생성 수정
6 demo_board > demo_helpers.py    

"/" route하는 Page를 등록하지 않고 구동 후 url 요청하면 에러 발생

 

결과 Table 화면 (http://localhost:3000/servers)

결과 테이블

biz_servers.py (기존)

import pynecone as pc

from .demo_state import State # Substate of Base State
from .demo_helpers import navbar

class ServerState(State):
    """The Substate of base state for the server page."""
    print("──────────────────── [ ServerState(State) ] ────────────────────")
    app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명

def servers():
    return pc.box(
        pc.vstack(
            navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가 
            pc.cond(
                State.logged_in,
                pc.box(
                    pc.vstack(
                        pc.heading("서버 모니터링 페이지", font_size="2em"),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/login",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="5em",
            width="100%",
        ),
    )

demo_server.py 화면을 구성하기위한 데이터는 따로 python 서비스 를 구현해서 데이터를 List[Dict] Type으로 리턴 받는다는 가정하에 샘플데이터를 만들어서 구현하겠습니다.

 

추가되는 함수 설명

def get_data()                                         # Table, DataTable을 위한 Sample List[Dict] 데이터 리턴

def render_tbody_tr(tr_data, index)     # Table thead 

def render_table(ServerState)              # Table 생성

def render_datatable(ServerState).     # DataTable 생성

 

샘플 데이터 

def get_data():
    # 샘플 데이터 List[Dict]
    result_list: List[Dict] = [
        {"Server Type": "WEB", "Server Name": "alpha", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "brovo", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "charlie", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "delta", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "echo", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
    ]
    pd_result_list = pd.DataFrame(result_list)
    pd_result_list=pd_result_list.fillna('')   # NaN값을 0 or ''로 치환한다.
    pd_result_list=pd_result_list.astype(str)  # 전체 Field를 str로 변환한다.

    return pd_result_list

Table을 추가

def render_tbody_tr(tr_data, index):
    return pc.tr(
        pc.td(index + 1),
        pc.foreach(tr_data, lambda data: pc.td(data)),
    )

def render_table(ServerState):
    return pc.box(
        pc.vstack(
            pc.table(
                pc.table_caption(ServerState.table_name),
                pc.thead(
                    pc.tr(
                        pc.td("#"),
                        pc.foreach(ServerState.table_columns, lambda data: pc.td(data)),
                    ),
                    bg="navy",
                    color="white",
                ),
                pc.tbody(
                    pc.foreach(ServerState.table_data, lambda data, i: render_tbody_tr(data, i)),
                ),
            ),
            border_width="1px",
            overflow="auto",
        ),
        padding_top="1em",
        width="98%",
    )

 

DataTable을 추가해보겠습니다.

DataTable은 자동으로 Table Header를 생성하기 위해서 앞서 생성한 List[Dict]를 Dataframe으로 변환한 데이터를 사용합니다.

def render_datatable(ServerState):
    return pc.box(
        pc.vstack(
            pc.data_table(
                data=ServerState.pd_result_list,
                pagination=True,
                search=True,
                sort=False, #True, False
                resizable=True,
                border_color="#43464B",
            ),
            width="100%",
            #align_items="start",
            #align_items="left",
            align_items=ServerState.box_align,
            #padding_x="15%",
            overflow="auto",
            #overflow_x="scroll",
            #overflow_y="scroll",
        ),
        border_width="0px",
        border_color="#43464B",
        width="98%",
    )

 

biz_servers.py (수정)

import pynecone as pc

import pandas as pd
from typing import List, Dict

from .demo_state import State # Substate of Base State
from .demo_helpers import navbar

def get_data():
    # 샘플 데이터 List[Dict]
    result_list: List[Dict] = [
        {"Server Type": "WEB", "Server Name": "alpha", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "brovo", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "charlie", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "delta", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
        {"Server Type": "WEB", "Server Name": "echo", "Total Disk": "100G", "Used Disk": "50G", "Available Disk": "50G", "HTTPS Status": "running", "Last Access": "17/Feb/2023:17:13:51"},
    ]
    pd_result_list = pd.DataFrame(result_list)
    pd_result_list=pd_result_list.fillna('')   # NaN값을 0 or ''로 치환한다.
    pd_result_list=pd_result_list.astype(str)  # 전체 Field를 str로 변환한다.

    return pd_result_list


class ServerState(State):
    """The Substate of base state for the server page."""
    print("──────────────────── [ ServerState(State) ] ────────────────────")
    app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명

    pd_result_list: List[Dict] = get_data()
    #print(pd_result_list)

    # table_columns : Dict에서 Columns 정보를 가져와서 Table의 thead를 구성
    # table_data : Dataframe에서 value만 List로 만들어서 Table의 tbody를 구성
    # table_name : Table의 table_caption 

    table_columns = list(pd_result_list.columns)
    table_data: List[List[str]] = pd_result_list.values.tolist()
    table_name: str = "WEB 서버 상태 확인"

    box_align: str = ""

def servers():
    return pc.box(
        pc.vstack(
            navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가 
            pc.cond(
                State.logged_in,
                pc.box(
                    pc.vstack(
                        pc.heading("서버 모니터링", font_size="2em"),
                        #pc.heading("서버 모니터링"),
                        render_table(ServerState),
                        pc.divider(),
                        render_datatable(ServerState),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/login",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="5.5em",
            width="100%",
        ),
    )

def render_tbody_tr(tr_data, index):
    return pc.tr(
        pc.td(index + 1),
        pc.foreach(tr_data, lambda data: pc.td(data)),
    )

def render_table(ServerState):
    return pc.box(
        pc.vstack(
            pc.table(
                pc.table_caption(ServerState.table_name),
                pc.thead(
                    pc.tr(
                        pc.td("#"),
                        pc.foreach(ServerState.table_columns, lambda data: pc.td(data)),
                    ),
                    bg="navy",
                    color="white",
                ),
                pc.tbody(
                    pc.foreach(ServerState.table_data, lambda data, i: render_tbody_tr(data, i)),
                ),
            ),
            border_width="1px",
            overflow="auto",
        ),
        padding_top="1em",
        width="98%",
    )

def render_datatable(ServerState):
    return pc.box(
        pc.vstack(
            pc.data_table(
                data=ServerState.pd_result_list,
                pagination=True,
                search=True,
                sort=False, #True, False
                resizable=True,
                border_color="#43464B",
            ),
            width="100%",
            #align_items="start",
            #align_items="left",
            align_items=ServerState.box_align,
            #padding_x="15%",
            overflow="auto",
            #overflow_x="scroll",
            #overflow_y="scroll",
        ),
        border_width="0px",
        border_color="#43464B",
        width="98%",
    )

 

결과 화면

Table과 DataTable이 아래위로 표시됩니다.

 

 

사용자 등록 후 로그인이 정상적으로 되면, Navigation Bar 오른쪽 상단에(붉은 사각형 영역) 로그인 사용자 정보, 다른 페이지 이동 및 로그아웃을 포함 하는 메뉴를 추가해보겠습니다.

 

https://pynecone.io/docs/library/media/avatar 참고

 

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py    
2 demo_board > demo_board.py    
3 demo_board > demo_state.py /logout 추가 수정
4 demo_board > demo_auth.py    
5 demo_board > demo_servers.py    
6 demo_board > demo_helpers.py navbar 오른쪽 상단에 Menu를 추가 수정

 

 

demo_state.py

logout 이 호출되면 State에 저장된값을 모두 reset 시키고 "/login" 페이지로 redirect.

import pynecone as pc

class User(pc.Model, table=True): 
    """A table of Users."""

    username: str 
    password: str
    userrole: str

class State(pc.State):
    """The base state for the app."""
    print("──────────────────── [ State(pc.State) ] ────────────────────")

    username: str
    userrole: str
    logged_in: bool = False

    def logout(self):
        """Log out a user."""
        self.reset()
        return pc.redirect("/login")

 

demo_helpers.py (기존)

import pynecone as pc
from .demo_state import State

def navbar(State, app_name):
    """The navbar."""
    return pc.box(
        pc.hstack(
            pc.hstack(
                pc.image(src="bada.png", width="48px"),
                pc.heading("Demo Board"),
                pc.heading(" - "+app_name, size="lg", color="navy"),
            ),
            pc.spacer(),
            justify="space-between",
            border_bottom="0.15em solid #d3d3d3",
            padding_x="2em",
            padding_y="1em",
            bg="rgba(255,255,255, 1)",
        ),
        position="fixed",
        width="100%",
        top="0px",
        z_index="500",
    )

 

demo_helpers.py (변경후)

매뉴에는 사용자 이름, 사용자 권한, 등록되어있는 페이지, 로그아웃을 포함.

import pynecone as pc
from .demo_state import State

def navbar(State, app_name):
    """The navbar."""
    return pc.box(
        pc.hstack(
            pc.hstack(
                pc.image(src="bada.png", width="48px"),
                pc.heading("Demo Board"),
                pc.heading(" - "+app_name, size="lg", color="navy"),
            ),
            pc.spacer(),
            pc.menu(
                pc.menu_button(
                    pc.cond(
                        State.logged_in,
                        pc.avatar(name=State.username, size="md"),
                        pc.box(),
                    )
                ),
                pc.menu_list(
                    pc.center(
                        pc.vstack(
                            pc.avatar(name=State.username, size="md"),
                            pc.text(State.username+"("+State.userrole+")"),
                        )
                    ),
                    pc.menu_divider(),
                    pc.link(pc.menu_item("Servers"),href="/servers"),
                    pc.menu_divider(),
                    pc.link(pc.menu_item("Sign Out"), on_click=State.logout),
                ),
            ),
            justify="space-between",
            border_bottom="0.15em solid #d3d3d3",
            padding_x="2em",
            padding_y="1em",
            bg="rgba(255,255,255, 1)",
        ),
        position="fixed",
        width="100%",
        top="0px",
        z_index="500",
    )

 

기본적인 화면 및 State 구성을 했으니 실제 로그인/로그아웃을 구현해보겠습니다.

 

로그인에 사용할 사용자 정보를 관리하기위해서 pynecone이 기본으로 제공하는 SQLite 파일 DB를 사용합니다.

pynecone docs에 보면 pc.session 을 통해서 SQLAlchemy syntax의 query로 Database에 데이터를 등록, 변경, 삭제, 조회 할수 있습니다. (https://pynecone.io/docs/database/queries)

"with pc.session" 으로 데이터처리를 하고 해당 block이 끝나면 자동으로 session도 닫게 됩니다.

 

이제 구현방법입니다.

- 먼저, State(pc.State) 에서 전 App에서 사용가능한 사용자 정보(이름, 패스워드, 권한)를 포함하는 User Class 생성하고 Base State에 로그인 한 사용자, 권한, 상태를 저장하는 vars를 생성

- demo_auth.py의 login에 사용자이름, 패스워드를 입력받는 로그인 Page 생성

- 사용자 등록하는 signup Page 생성 및 사용자 등록

- DB Browser for SQLite를 이용해서 직접 pynecondb에 붙어서 기본 사용자를확인 

 

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구분
1 pcconfig.py pc 구동 설정 파일  
2 demo_board > demo_board.py "/signup" 사용자 등록 Page 추가
"/servers" Page 추가
수정
3 demo_board > demo_state.py 나중 추가 확장성(페이지추가)를 위해 Base State(pc.State)  수정
4 demo_board > demo_auth.py "/login" 페이지 변경 및 "/signup" 페이지 추가 수정
5 demo_board > demo_servers.py 로그인후 오픈할 페이지 추가
6 demo_board > demo_helpers.py    

 

https://pynecone.io/docs/database/tables

 

demo_state.py 수정

import pynecone as pc

#table=True 인수는 Pynecone에게 이 클래스에 대해 데이터베이스에 테이블을 생성하도록 지시
class User(pc.Model, table=True): 
    """A table of Users."""

    username: str                 # 사용자명
    password: str                 # 패스워드
    userrole: str                 # 사용자 권한

class State(pc.State):
    """The base state for the app."""
    print("──────────────────── [ State(pc.State) ] ────────────────────")

    username: str
    userrole: str
    logged_in: bool = False       # 로그인/로그아웃시 상태를 저장

 

demo_auth.py 수정

Log in / Sign up 을위해서 추가 변경되는 부분이 많습니다.

import pynecone as pc

from .demo_state import State , User # Base State, User Class import
from .demo_helpers import navbar

# 로그인 호면에 대한 스타일 시트 적용
styles = {
    "login_page": {
        "padding_top": "10em",
        "text_align": "top",
        "position": "relative",
        "background_image": "bg.svg",
        "background_size": "100% auto",
        "width": "100%",
        "height": "100vh",
    },
    "login_input": {
        "shadow": "lg",
        "padding": "1em",
        "border_radius": "lg",
        "background": "white",
    },
}

class AuthState(State):
    """The Substate of base state for the login page."""
    print("──────────────────── [ AuthState(State) ] ────────────────────")
    app_name: str = "Log In"

    password: str           # 로그인에서 입력받은 Password

    new_username: str       # 신규 사용자 Name
    new_userrole: str       # 신규 사용자 Role
    new_password: str       # 신규 사용자 Password
    confirm_password: str   # 신규 사용자 Confirm Password

    message: str            # 사용자 등록시 발생하는 Error 등 메시지 표시
    message_color: str = "red" # Message의 기본색은 red, Sign up 성공인경우 navy

    def signup(self):
        """Sign up a user."""
        print("signup values : [{}], [{}], [{}]".format(self.new_username, self.new_password, self.new_userrole))
        with pc.session() as session:
            #self.message_color = "red"
            # 입력 값 Validation 체크
            if self.new_userrole == "" or self.new_userrole == None:
                self.message = "Select user role."
                return
            if self.new_username == "" or self.new_username == None:
                self.message = "Input user name."
                return
            if self.new_password == "" or self.new_password == None:
                self.message = "Input passwd"
                return
            if self.new_password != self.confirm_password:
                self.message = "Passwords do not match."
                return
            if session.exec(User.select.where(User.username == self.new_username)).first():
                self.message = "Username already exists."
                return

            user = User(username=self.new_username, password=self.new_password, userrole=self.new_userrole)
            session.add(user)
            session.commit()
            self.logged_in = False
            self.message_color = "navy"
            self.message = "User siginup completed, please log in"
            return pc.redirect("/login")

    def login(self):
        """Log in a user."""
        if self.username == "" or self.password=="":
            self.message = "Input username and password."
            return

        with pc.session() as session:
            user = session.exec(
                User.select.where(User.username == self.username)
            ).first()
            if user and user.password == self.password:
                self.logged_in = True
                self.userrole = user.userrole
                return pc.redirect("/servers")
            else:
                self.message = "Invalid username or password."
                return

    # Signup 화면에서 선택한 Role 정보를 AuthState에 저장
    def role_change(self,value):
        print("role_change value : {}".format(value))
        self.new_userrole = value

    def clear_message(self):
        self.message = ""
        self.message_color = "red"


def signup():
    return pc.box(
        pc.vstack(
            navbar(State, AuthState.app_name),
            pc.center(
                pc.vstack(
                    pc.heading("Sign Up", font_size="1.5em"),
                    pc.select(
                        ["User", "Admin"],
                        placeholder="Select a Role.",
                        on_change=lambda value: AuthState.role_change(value),
                        border_style="solid",
                        border_width="1px",
                        border_color="#43464B",
                    ),
                    pc.input(
                        on_blur=AuthState.set_new_username, placeholder="Username", width="100%", 
                        on_click=AuthState.clear_message,
                    ),
                    pc.input(
                        on_blur=AuthState.set_new_password,
                        placeholder="Password",
                        type_="password",
                        width="100%",
                        on_click=AuthState.clear_message,
                    ),
                    pc.input(
                        on_blur=AuthState.set_confirm_password,
                        placeholder="Confirm Password",
                        type_="password",
                        width="100%",
                        on_click=AuthState.clear_message,
                    ),
                    pc.button("Sign Up", on_click=AuthState.signup, width="100%"),
                    pc.text(AuthState.message, color=AuthState.message_color),
                ),
                style=styles["login_input"],
            ),
        ),
        style=styles["login_page"],
    )

def login():
    return pc.box(
        pc.vstack(
            navbar(State, AuthState.app_name),
            pc.center(
                pc.vstack(
                    pc.heading("Log In", font_size="1.5em"),
                    pc.input(
                        on_blur=State.set_username, placeholder="Username", width="100%", 
                        on_click=AuthState.clear_message,
                    ),
                    pc.input(
                        on_change=AuthState.set_password,
                        placeholder="Password",
                        type_="password",
                        width="100%",
                        on_click=AuthState.clear_message,
                    ),
                    # Username, Password를 입력받으면 AuthState의 login 함수 호출하는 event 작동
                    pc.button("Login", on_click=AuthState.login, width="100%"),  
                    # 사용자 등록하기위한 Signup
                    pc.link(
                        pc.button("Sign Up", width="100%"), href="/signup", width="100%"
                    ),
                    pc.text(AuthState.message, color=AuthState.message_color),
                ),
                style=styles["login_input"], # 로그인 Page에 맞는 추가 스타일 
            ),
        ),
        style=styles["login_page"],
    )

 

demo_servers.py

"/servers" 페이지는 pc.cond 컴포넌트로 Base State의 logged_in var가 True 일때만 열리게 구성합니다.

import pynecone as pc

from .demo_state import State # Substate of Base State
from .demo_helpers import navbar

class ServerState(State):
    """The Substate of base state for the server page."""
    print("──────────────────── [ ServerState(State) ] ────────────────────")
    app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명

def servers():
    return pc.box(
        pc.vstack(
            navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가 
            pc.cond(
                State.logged_in,
                pc.box(
                    pc.vstack(
                        pc.heading("서버 모니터링 페이지", font_size="2em"),
                    ),
                    width="100%",
                    border_width="0px",
                ),
                pc.link(
                    pc.button("먼저 로그인 하세요"),
                    href="/login",
                    color="rgb(107,99,246)",
                    button=True,
                )
            ),
            padding_top="5em",
            width="100%",
        ),
    )

로그인 없이 접근할경우

login 없이 접근할때

 

demo_board.py

"""Welcome to Pynecone! This file outlines the steps to create a basic app."""
from pcconfig import config

import pynecone as pc
from .demo_state import State
from .demo_auth import login, signup # Page 추가
from .demo_servers import servers

def index():
    return pc.center(
        pc.vstack(
            pc.heading("Pynecone Demo Board!", font_size="2em"),
            pc.link(
                "로그인",
                href="/login",
                border="0.1em solid",
                padding="0.5em",
                border_radius="0.5em",
                _hover={
                    "color": "rgb(107,99,246)",
                },
            ),
            spacing="1.5em",
            font_size="2em",
        ),
        padding_top="10%",
    )


# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index)
app.add_page(login)
app.add_page(signup) # Page 추가
app.add_page(servers) # Page 추가
app.compile()

 

서버 재시작 (ctrl + c & pc run)

Log in page
Sign up page

 

서버 재구동후 DB Browser for SQLite 로 pynecone.db 파일을 열어보면 user 테이블이 생성된것을 확인

 

DB Browser for SQLite 를 닫고 사용자를 추가 (Sign up)

 

사용자 정보 입력 후 Sign Up
사용자 등록 완료후 로그인 페이지

pynecone.db에 저장된 사용자 확인 

(문서 만들기 시작할때는 낮이었는데 이제 밤이라 야간모드로 전환되었네요)

 

로그인

등록된 ID/PW 입력

로그인 성공 후 /servers Page로 이동

 

프로젝트 명(폴더) : demo_board

# 파일 경로 설명 구현 여부
1 pcconfig.py pc 구동 설정 파일 기본생성
2 demo_board > demo_board.py 첫 index 페이지 ("/") 기본생성, 일부수정
3 demo_board > demo_state.py 나중 추가 확장성(페이지추가)를 위해 Base State(pc.State)  추가
4 demo_board > demo_auth.py "/login" 페이지 추가
5 demo_board > demo_helpers.py Navigation Bar 화면 생성 추가

#3 demo_state.py 추가

- 아직은 빈 Base State class

import pynecone as pc

class State(pc.State):
    """The base state for the app."""
    print("──────────────────── [ State(pc.State) ] ────────────────────")

 

#4 demo_auth.py 생성

import pynecone as pc

from .demo_state import State # Substate of Base State

class AuthState(State):
    """The Substate of base state for the login page."""
    print("──────────────────── [ AuthState(State) ] ────────────────────")

def login():
    return pc.box(
        pc.vstack(
            pc.heading("로그인 페이지", font_size="2em"),
            padding_top="5em",
            width="100%",
        ),
    )

 

 

#2 demo_board.py 수정

- State(pc.State) 클래스 제거,

- 앞에서 생성한 demo_state.py의 Base State import

- link를 추가한 demo_auth.py의  /login Page로 변경

"""Welcome to Pynecone! This file outlines the steps to create a basic app."""
from pcconfig import config

import pynecone as pc
from .demo_state import State  #1 새로 생성한 demo_state.py page의 State import
from .demo_auth import login   #2 새로 생성한 login page를 import

def index():
    return pc.center(
        pc.vstack(
            pc.heading("Pynecone Demo Board!", font_size="2em"),
            pc.link(
                "로그인",              #3 화면 내용 수정
                href="/login",
                border="0.1em solid",
                padding="0.5em",
                border_radius="0.5em",
                _hover={
                    "color": "rgb(107,99,246)",
                },
            ),
            spacing="1.5em",
            font_size="2em",
        ),
        padding_top="10%",
    )


# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index)
app.add_page(login) #4 로그인 페이지를 App에 등록
app.compile()

실행 - pc run

"/" 페이지 수정

#5 demo_helpers.py 추가

로그인 Page 및 추가될 Page들에서 공통으로 사용할 Navigation Bar를 화면 상단에 fix 크기로 생성합니다.

import pynecone as pc
from .demo_state import State # Base State의 Vars들을 사용하기위해서 import

def navbar(State, app_name): #1 Nav.Bar 상단에 각 Page의 이름을 표시하기위해서 파라미터로 받음.
    """The navbar."""
    return pc.box(
        pc.hstack(
            pc.hstack(
                pc.image(src="bada.png", width="48px"), #2 회사 로고가있어서 좌측상단에 표시
                pc.heading("Demo Board")                #3 로고 옆에 프로젝트 이름
                pc.heading(" - "+app_name, size="lg", color="navy"), #4 Page 이름 표시
            ),
            pc.spacer(),
            justify="space-between",
            border_bottom="0.15em solid #d3d3d3",
            padding_x="2em",
            padding_y="1em",
            bg="rgba(255,255,255, 1)",
        ),
        position="fixed", #5 Box의 위치 및 크기를 지정후 고정한다.
        width="100%",
        top="0px",
        z_index="500",
    )

생성한 Navigation Bar를 login Page에 붙여 넣기

- login page 의 이름을 지정하고

- navbar를 추가할때 page 이름을 파라미터로 전달

import pynecone as pc

from .demo_state import State # Substate of Base State

class AuthState(State):
    """The Substate of base state for the login page."""
    print("──────────────────── [ AuthState(State) ] ────────────────────")
    app_name: str = "Log In" #1 Navigation Bar에 표시될 Page 명

def login():
    return pc.box(
        pc.vstack(
            navbar(State, AuthState.app_name), #2 맨위 상단에 navbar를 추가 
            pc.heading("로그인 페이지", font_size="2em"),
            padding_top="5em",
            width="100%",
        ),
    )

실행

다음 페이지 : https://amnesia.tistory.com/10

+ Recent posts