Compare commits

...

64 Commits

Author SHA1 Message Date
xue jiahao
fd9d590be2 [UI] version page 2024-12-06 23:29:16 +08:00
xue jiahao
eaedab505c Add save version dialog
# Conflicts:
#	frontend/src/types/AppTypes.ts
2024-12-06 13:04:42 +08:00
xue jiahao
1786ea920a fix ui 2024-12-06 12:59:41 +08:00
xue jiahao
c8bb551ed1 Fix text 2024-12-06 11:45:27 +08:00
xiaozhe.ma
e616f0c142 ドメイン文言修正 2024-12-06 11:12:55 +09:00
xiaozhe.ma
bfa85fab41 source merge 2024-12-06 11:00:23 +09:00
xiaozhe.ma
d3478ef851 modified japanese 2024-12-06 10:58:07 +09:00
xue jiahao
b26877ef58 [UI] load apps fail warning 2024-12-05 17:55:12 +08:00
xue jiahao
4336462ff1 some UI 2024-12-05 17:41:00 +08:00
a6576827fd Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-04 21:45:00 +09:00
b98c20d7ff bugfix get/apps 2024-12-04 21:44:49 +09:00
xue jiahao
93f44282d3 fix 2024-12-04 16:49:29 +08:00
adb0df3b17 AbstractPage -> update the create for new version 2024-12-04 15:03:41 +09:00
4f17a6952d fix pytest 2024-12-04 14:44:32 +09:00
c5c4f79e4f bugfix dbcrud 2024-12-04 13:21:19 +09:00
6504d8d29f refactor: dbcrud->dbuser 2024-12-03 22:12:30 +09:00
9f61ab300c Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-03 16:53:26 +09:00
0c384bf57b mani.py on_event->lifespan; error->return code -1 2024-12-03 16:53:18 +09:00
xue jiahao
a7860ed94a UI bugfix 2024-12-03 15:23:03 +08:00
25ee2f8747 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-03 15:18:16 +09:00
0d396024cb bugfix get_user 2024-12-03 15:18:09 +09:00
xue jiahao
2a76f5a4c7 add owner column 2024-12-03 12:23:20 +08:00
xue jiahao
dcfe0d44fd add delete hint 2024-12-03 11:10:25 +08:00
xue jiahao
660ffe36c2 Add shared dialog 2024-12-02 23:41:23 +08:00
xue jiahao
8a3aaec8d5 [UI] update card 2024-12-02 21:00:42 +08:00
f13d1d51ca Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-02 21:59:43 +09:00
bc52daa46a UserOut ->add id 2024-12-02 21:59:34 +09:00
xue jiahao
e3d842de15 Fix api call and delete tenantid on domain page 2024-12-02 12:52:59 +08:00
ff46485498 bugfix kintone:get_default_domain 2024-12-01 12:42:14 +09:00
d23e16d1eb kintone: get_activedomain->get_default_domain 2024-12-01 12:36:46 +09:00
8ee013527a bugfix get_default_domain 2024-12-01 12:21:13 +09:00
39b02e0a8e get_activedomain->dbdomain.get_default_domain 2024-12-01 12:01:31 +09:00
647a5f4b8e delete create_domain repeat check 2024-12-01 11:46:23 +09:00
f3b93dc426 bugfix delete_domain 2024-12-01 11:40:46 +09:00
c0feb74a13 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-01 11:19:58 +09:00
77516b8814 refactor the crud & paginate 2024-12-01 11:19:51 +09:00
xue jiahao
bca2f46ea5 [UI] Fix behavious when delete user domain 2024-11-27 18:05:48 +08:00
xue jiahao
024645e16a [UI] dialog and redirect for no active domain 2024-11-27 16:01:18 +08:00
xue jiahao
df5b012bcd fix api call result & some UI improve 2024-11-27 15:51:57 +08:00
49d9475304 add add_admindomain for admin 2024-11-27 16:51:24 +09:00
41aa11720d fix again 2024-11-27 16:44:06 +09:00
01b3e8b8b5 fix delete_domain 2024-11-27 16:43:05 +09:00
3726c8f342 fix active_userdomain 2024-11-27 16:36:53 +09:00
2b4f4292a8 fix setuserdomain& activedomian 2024-11-27 16:22:49 +09:00
3b15dabedc fix create_userdomain 2024-11-27 16:12:41 +09:00
aa7daf4447 fix activedomain 2024-11-27 15:56:26 +09:00
xue jiahao
c5048a2ac3 fix api call result 2024-11-27 11:26:17 +08:00
0232e0d2c2 GenericModel -> BaseModel 2024-11-26 17:57:25 +09:00
97d1232def add ApiReturnModel ー>return value 統一 2024-11-26 16:31:54 +09:00
3447c7832c remove return no exception when no domain 2024-11-26 15:22:53 +09:00
65a7db7b3f Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-11-26 15:19:04 +09:00
1f176d40bc get_domains 改修 2024-11-26 15:18:56 +09:00
xue jiahao
af77632da5 Some ts interface fix 2024-11-25 19:06:15 +08:00
xue jiahao
7a1ff8ac30 [UI] inactive domain 2024-11-25 18:51:47 +08:00
cc726c7f68 set domain inactive -> userdomain.active =false 2024-11-25 18:36:21 +09:00
0d825f6387 bugfix edit_domain is_active更新漏れ 2024-11-25 18:20:45 +09:00
1db7d66648 edit_domain password以外の修正 2024-11-25 18:17:37 +09:00
440a0bd647 add active ,onwer in domain 2024-11-24 21:01:21 +09:00
xiaozhe.ma
0fddeaa036 権限変更修正 2024-11-24 19:00:17 +09:00
xue jiahao
4cd3aff868 Merged PR 7: Add new application 2024-11-24 07:55:15 +00:00
xiaozhe.ma
055ec1aeaf merge with backend 2024-11-24 16:53:47 +09:00
方 柏
321f14b229 Merged PR 8: app&appversion&flowhistory&role&permission
app&appversion&flowhistory&role&permission
2024-11-23 10:49:59 +00:00
xue jiahao
bf4abe3cad Fix select app 2024-11-22 12:45:37 +08:00
xue jiahao
3f98e17215 [feature] add new application 2024-11-20 16:05:18 +08:00
51 changed files with 2786 additions and 821 deletions

View File

@@ -9,15 +9,16 @@ import app.core.config as config
import os
from pathlib import Path
from app.db.session import SessionLocal
from app.db.crud import get_flows_by_app,get_activedomain,get_kintoneformat
from app.db.crud import get_flows_by_app,get_kintoneformat
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
from app.db.cruddb.dbdomain import dbdomain
kinton_router = r = APIRouter()
def getkintoneenv(user = Depends(get_current_user)):
db = SessionLocal()
domain = get_activedomain(db, user.id)
domain = dbdomain.get_default_domain(db,user.id) #get_activedomain(db, user.id)
db.close()
kintoneevn = config.KINTONE_ENV(domain)
return kintoneevn

View File

@@ -9,16 +9,30 @@ from app.db.schemas import *
from typing import List, Optional
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
from app.core.common import ApiReturnModel,ApiReturnPage
#from fastapi_pagination import Page
from app.db.cruddb import dbdomain
import httpx
import app.core.config as config
platform_router = r = APIRouter()
@r.get(
"/test",
response_model_exclude_none=True,
)
async def test(
request: Request,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
dbdomain.select(db,{"tenantid":1,"name":["b","c"]})
@r.get(
"/apps",
response_model=List[AppList],
"/apps",tags=["App"],
response_model=ApiReturnModel[List[AppList]|None],
response_model_exclude_none=True,
)
async def apps_list(
@@ -27,8 +41,11 @@ async def apps_list(
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
domain = dbdomain.get_default_domain(db,user.id) #get_activedomain(db, user.id)
if not domain:
return ApiReturnModel(data = None)
filtered_apps = []
platformapps = get_apps(db,domain.url)
kintoneevn = config.KINTONE_ENV(domain)
headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE}
@@ -47,15 +64,15 @@ async def apps_list(
offset += limit
kintone_apps_dict = {app['appId']: app for app in all_apps}
filtered_apps = []
for papp in platformapps:
if papp.appid in kintone_apps_dict:
papp.appname = kintone_apps_dict[papp.appid]["name"]
filtered_apps.append(papp)
return filtered_apps
return ApiReturnModel(data = filtered_apps)
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
@r.post("/apps", response_model=AppList, response_model_exclude_none=True)
async def apps_update(
request: Request,
@@ -191,7 +208,7 @@ async def flow_details(
@r.get(
"/flows/{appid}",
response_model=List[Flow],
response_model=List[Flow|None],
response_model_exclude_none=True,
)
async def flow_list(
@@ -201,7 +218,9 @@ async def flow_list(
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
domain = dbdomain.get_default_domain(db, user.id) #get_activedomain(db, user.id)
if not domain:
return []
print("domain=>",domain)
flows = get_flows_by_app(db, domain.url, appid)
return flows
@@ -209,7 +228,7 @@ async def flow_list(
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@r.post("/flow", response_model=Flow, response_model_exclude_none=True)
@r.post("/flow", response_model=Flow|None, response_model_exclude_none=True)
async def flow_create(
request: Request,
flow: FlowIn,
@@ -217,14 +236,16 @@ async def flow_create(
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
domain = dbdomain.get_default_domain(db, user.id) #get_activedomain(db, user.id)
if not domain:
return None
return create_flow(db, domain.url, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@r.put(
"/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
"/flow/{flowid}", response_model=Flow|None, response_model_exclude_none=True
)
async def flow_edit(
request: Request,
@@ -234,7 +255,9 @@ async def flow_edit(
db=Depends(get_db),
):
try:
domain = get_activedomain(db, user.id)
domain = dbdomain.get_default_domain(db, user.id) #get_activedomain(db, user.id)
if not domain:
return None
return edit_flow(db,domain.url, flow,user.id)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@@ -254,51 +277,60 @@ async def flow_delete(
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
@r.get(
"/domains/{tenantid}",
response_model=List[Domain],
"/domains",tags=["Domain"],
response_model=ApiReturnPage[Domain],
response_model_exclude_none=True,
)
async def domain_details(
request: Request,
tenantid:str,
db=Depends(get_db),
):
try:
domains = get_domains(db,tenantid)
return domains
except Exception as e:
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
@r.post("/domain", response_model=Domain, response_model_exclude_none=True)
async def domain_create(
request: Request,
domain: DomainBase,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return create_domain(db, domain,user.id)
if user.is_superuser:
domains = dbdomain.get_domains(db)
else:
domains = dbdomain.get_domains_by_owner(db,user.id)
return domains
except Exception as e:
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
@r.post("/domain", tags=["Domain"],
response_model=ApiReturnModel[Domain],
response_model_exclude_none=True)
async def domain_create(
request: Request,
domain: DomainIn,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = dbdomain.create_domain(db, domain,user.id))
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@r.put(
"/domain", response_model=Domain, response_model_exclude_none=True
"/domain", tags=["Domain"],
response_model=ApiReturnModel[Domain|None],
response_model_exclude_none=True
)
async def domain_edit(
request: Request,
domain: DomainBase,
domain: DomainIn,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return edit_domain(db, domain,user.id)
return ApiReturnModel(data = dbdomain.edit_domain(db, domain,user.id))
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
@r.delete(
"/domain/{id}", response_model=Domain, response_model_exclude_none=True
"/domain/{id}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def domain_delete(
request: Request,
@@ -306,13 +338,13 @@ async def domain_delete(
db=Depends(get_db),
):
try:
return delete_domain(db,id)
return ApiReturnModel(data = dbdomain.delete_domain(db,id))
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain:",e)
@r.get(
"/domain",
# response_model=List[Domain],
response_model=List[Domain],
response_model_exclude_none=True,
)
async def userdomain_details(
@@ -328,72 +360,93 @@ async def userdomain_details(
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@r.post(
"/domain/{userid}",
"/domain/{userid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def create_userdomain(
request: Request,
userid: int,
domainids:List[int] ,
domainid:int ,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = add_userdomain(db, userid,domainids)
return domain
if user.is_superuser:
domain = dbdomain.add_userdomain(db,user.id,userid,domainid)
else:
domain = dbdomain.add_userdomain_by_owner(db,user.id,userid,domainid)
return ApiReturnModel(data = domain)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e)
@r.delete(
"/domain/{domainid}/{userid}", response_model_exclude_none=True
"/domain/{domainid}/{userid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def userdomain_delete(
async def delete_userdomain(
request: Request,
domainid:int,
userid: int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return delete_userdomain(db, userid,domainid)
return ApiReturnModel(data = dbdomain.delete_userdomain(db,userid,domainid))
except Exception as e:
raise APIException('platform:delete',request.url._url,f"Error occurred while delete user({userid}) domain:",e)
@r.get(
"/activedomain",
response_model=Domain,
"/defaultdomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def get_useractivedomain(
async def get_defaultuserdomain(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
# domain = get_activedomain(db, user.id)
domain = get_activedomain(db, userId if userId is not None else user.id)
if domain is None:
return JSONResponse(content=None,status_code=HTTPStatus.OK)
return domain
return ApiReturnModel(data =dbdomain.get_default_domain(db, user.id))
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",e)
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e)
@r.put(
"/activedomain/{domainid}",
"/defaultdomain/{domainid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def update_activeuserdomain(
request: Request,
domainid:int,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domain = active_userdomain(db, userId if userId is not None else user.id,domainid)
return domain
domain = dbdomain.set_default_domain(db,user.id,domainid)
return ApiReturnModel(data= domain)
except Exception as e:
raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",e)
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e)
@r.get(
"/domainshareduser/{domainid}",tags=["Domain"],
response_model=ApiReturnPage[UserOut|None],
response_model_exclude_none=True,
)
async def get_domainshareduser(
request: Request,
domainid:int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return dbdomain.get_shareddomain_users(db,user.id,domainid)
except Exception as e:
raise APIException('platform:sharedomain',request.url._url,f"Error occurred while get user({user.id}) sharedomain:",e)
@r.get(
"/events",

View File

@@ -1,8 +1,10 @@
from fastapi import APIRouter, Request, Depends, Response, Security, encoders
import typing as t
from app.core.common import ApiReturnModel,ApiReturnPage
from app.core.apiexception import APIException
from app.db.session import get_db
from app.db.crud import (
get_allusers,
get_users,
get_user,
create_user,
@@ -11,126 +13,174 @@ from app.db.crud import (
assign_userrole,
get_roles,
)
from app.db.schemas import UserCreate, UserEdit, User, UserOut,Role
from app.db.schemas import UserCreate, UserEdit, User, UserOut,RoleBase,Permission
from app.core.auth import get_current_user,get_current_active_user, get_current_active_superuser
from app.db.cruddb import dbuser
users_router = r = APIRouter()
@r.get(
"/users",
response_model=t.List[User],
"/users",tags=["User"],
response_model=ApiReturnPage[User],
response_model_exclude_none=True,
)
async def users_list(
response: Response,
request: Request,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
):
"""
Get all users
"""
users = get_users(db,current_user.is_superuser)
# This is necessary for react-admin to work
#response.headers["Content-Range"] = f"0-9/{len(users)}"
return users
try:
if current_user.is_superuser:
users = dbuser.get_users(db)
else:
users = dbuser.get_users_not_admin(db)
return users
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while get user list",e)
@r.get("/users/me", response_model=User, response_model_exclude_none=True)
@r.get("/users/me", tags=["User"],
response_model=ApiReturnModel[User],
response_model_exclude_none=True,
)
async def user_me(current_user=Depends(get_current_active_user)):
"""
Get own user
"""
return current_user
return ApiReturnModel(data = current_user)
@r.get(
"/users/{user_id}",
response_model=User,
"/users/{user_id}",tags=["User"],
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
)
async def user_details(
request: Request,
user_id: int,
db=Depends(get_db),
current_user=Depends(get_current_active_superuser),
current_user=Depends(get_current_active_user),
):
"""
Get any user details
"""
user = get_user(db, user_id)
return user
# return encoders.jsonable_encoder(
# user, skip_defaults=True, exclude_none=True,
# )
try:
user = dbuser.get(db, user_id)
if user:
if user.is_superuser and not current_user.is_superuser:
user = None
return ApiReturnModel(data = user)
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while get user({user_id}) detail:",e)
@r.post("/users", response_model=User, response_model_exclude_none=True)
@r.post("/users", tags=["User"],
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
)
async def user_create(
request: Request,
user: UserCreate,
db=Depends(get_db),
current_user=Depends(get_current_active_superuser),
current_user=Depends(get_current_active_user),
):
"""
Create a new user
"""
return create_user(db, user)
try:
if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None)
return ApiReturnModel(data =dbuser.create_user(db, user,current_user.id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while create user({user.email}):",e)
@r.put(
"/users/{user_id}", response_model=User, response_model_exclude_none=True
"/users/{user_id}", tags=["User"],
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
)
async def user_edit(
request: Request,
user_id: int,
user: UserEdit,
db=Depends(get_db),
current_user=Depends(get_current_active_superuser),
current_user=Depends(get_current_active_user),
):
"""
Update existing user
"""
return edit_user(db, user_id, user)
try:
if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None)
return ApiReturnModel(data = dbuser.edit_user(db,user_id,user,current_user.id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while edit user({user_id}):",e)
@r.delete(
"/users/{user_id}", response_model=User, response_model_exclude_none=True
"/users/{user_id}", tags=["User"],
response_model=ApiReturnModel[UserOut|None],
response_model_exclude_none=True
)
async def user_delete(
request: Request,
user_id: int,
db=Depends(get_db),
current_user=Depends(get_current_active_superuser),
current_user=Depends(get_current_active_user),
):
"""
Delete existing user
"""
return delete_user(db, user_id)
try:
user = dbuser.get(db,user_id)
if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None)
return ApiReturnModel(data = dbuser.delete_user(db, user_id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while delete user({user_id}):",e)
@r.post("/userrole",
response_model=User,
@r.post("/userrole",tags=["User"],
response_model=ApiReturnModel[User],
response_model_exclude_none=True,)
async def assign_role(
request: Request,
userid:int,
user_id:int,
roles:t.List[int],
db=Depends(get_db)
):
return assign_userrole(db,userid,roles)
try:
return ApiReturnModel(data = dbuser.assign_userrole(db,user_id,roles))
except Exception as e:
raise APIException('user:userrole',request.url._url,f"Error occurred while assign user({user_id}) roles({roles}):",e)
@r.get(
"/roles",
response_model=t.List[Role],
"/roles",tags=["User"],
response_model=ApiReturnModel[t.List[RoleBase]|None],
response_model_exclude_none=True,
)
async def roles_list(
response: Response,
request: Request,
db=Depends(get_db),
current_user=Security(get_current_active_user, scopes=["role_list"]),
current_user=Depends(get_current_active_user),
#current_user=Security(get_current_active_user, scopes=["role_list"]),
):
roles = get_roles(db)
return roles
try:
if current_user.is_superuser:
roles = dbuser.get_roles(db)
else:
if len(current_user.roles)>0:
roles = dbuser.get_roles_by_level(db,current_user.roles[0].level)
else:
roles = []
return ApiReturnModel(data = roles)
except Exception as e:
raise APIException('user:roles',request.url._url,f"Error occurred while get roles:",e)
@r.get(
"/userpermssions",tags=["User"],
response_model=ApiReturnModel[t.List[Permission]|None],
response_model_exclude_none=True,
)
async def permssions_list(
request: Request,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
#current_user=Security(get_current_active_user, scopes=["role_list"]),
):
try:
if current_user.is_superuser:
permissions = dbuser.get_permissions(db)
else:
if len(current_user.roles)>0:
permissions = dbuser.get_user_permissions(db,current_user.id)
else:
permissions = []
return ApiReturnModel(data = permissions)
except Exception as e:
raise APIException('user:userpermssions',request.url._url,f"Error occurred while get user({current_user.id}) permissions:",e)

View File

@@ -19,7 +19,7 @@ class APIException(Exception):
elif hasattr(e, 'detail'):
self.detail = e.detail
self.status_code = e.status_code if hasattr(e, 'status_code') else 500
content += e.detail
content += str(e.detail)
else:
self.detail = str(e)
self.status_code = 500

View File

@@ -6,7 +6,7 @@ from jwt import PyJWTError
from app.db import models, schemas, session
from app.db.crud import get_user_by_email, create_user,get_user
from app.core import security
from app.db.cruddb import dbuser
async def get_current_user(security_scopes: SecurityScopes,
db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme)
@@ -35,7 +35,7 @@ async def get_current_user(security_scopes: SecurityScopes,
token_data = schemas.TokenData(id = id, permissions=permissions)
except PyJWTError:
raise credentials_exception
user = get_user(db, token_data.id)
user = dbuser.get_user(db, token_data.id)
if user is None:
raise credentials_exception
return user

View File

@@ -0,0 +1,49 @@
import math
from fastapi import Query
from fastapi_pagination.bases import AbstractPage,AbstractParams,RawParams
from pydantic import BaseModel
from typing import Any, Generic, List, Type,TypeVar,Generic,Sequence
from fastapi_pagination import Page,utils
T = TypeVar('T')
class ApiReturnModel(BaseModel,Generic[T]):
code:int = 0
msg:str ="OK"
data:T
class ApiReturnError(BaseModel):
code:int = -1
msg:str =""
class Params(BaseModel, AbstractParams):
page:int = Query(1,get=1, description="Page number")
size:int = Query(20,get=0, le=100,description="Page size")
def to_raw_params(self) -> RawParams:
return RawParams(
limit=self.size,
offset=self.size*(self.page-1)
)
class ApiReturnPage(AbstractPage[T],Generic[T]):
code:int =0
msg:str ="OK"
data:Sequence[T]
total:int
page:int
size:int
# next:str
# previous:str
total_pages:int
__params_type__ =Params
@classmethod
def create(cls,items:Sequence[T],params:Params,**kwargs: Any) -> Type[Page[T]]:
total = kwargs.get('total', 0)
total_pages = math.ceil(total/params.size)
return utils.create_pydantic_model(cls,data=items,total=total,page=params.page,size=params.size,total_pages=total_pages)

View File

@@ -1,4 +1,4 @@
import datetime
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import and_
@@ -19,13 +19,15 @@ def get_user_by_email(db: Session, email: str) -> schemas.UserBase:
return db.query(models.User).filter(models.User.email == email).first()
def get_users(
db: Session, super:bool
def get_allusers(
db: Session
) -> t.List[schemas.UserOut]:
if super:
return db.query(models.User).all()
else:
return db.query(models.User).filter(models.User.is_superuser == False)
return db.query(models.User).all()
def get_users(
db: Session
) -> t.List[schemas.UserOut]:
return db.query(models.User).filter(models.User.is_superuser == False)
def create_user(db: Session, user: schemas.UserCreate):
@@ -76,7 +78,7 @@ def edit_user(
def get_roles(
db: Session
) -> t.List[schemas.Role]:
) -> t.List[schemas.RoleBase]:
return db.query(models.Role).all()
def assign_userrole( db: Session, user_id: int, roles: t.List[int]):
@@ -246,14 +248,13 @@ def edit_flow(
#見つからない時新規作成
return create_flow(db,domainurl,flow,userid)
db_flow.appid =flow.appid,
db_flow.eventid=flow.eventid,
db_flow.domainurl=domainurl,
db_flow.name=flow.name,
db_flow.content=flow.content,
db_flow.updateuserid = userid,
db_flow.update_time = datetime.now
db_flow.appid =flow.appid
db_flow.eventid=flow.eventid
db_flow.domainurl=domainurl
db_flow.name=flow.name
db_flow.content=flow.content
db_flow.updateuserid = userid
db.add(db_flow)
db.commit()
db.refresh(db_flow)
@@ -278,57 +279,78 @@ def get_flows_by_app(db: Session,domainurl: str, appid: str):
raise Exception("Data not found")
return flows
def create_domain(db: Session, domain: schemas.DomainBase,userid:int):
def create_domain(db: Session, domain: schemas.DomainIn,userid:int):
domain.encrypt_kintonepwd()
db_domain = models.Domain(
tenantid = domain.tenantid,
name=domain.name,
url=domain.url,
is_active=domain.is_active,
kintoneuser=domain.kintoneuser,
kintonepwd=domain.kintonepwd,
createuserid = userid,
updateuserid = userid
updateuserid = userid,
ownerid = domain.ownerid
)
db.add(db_domain)
db.flush()
add_userdomain(db,userid,db_domain.id)
#add_userdomain(db,userid,db_domain.id)
db.commit()
db.refresh(db_domain)
return db_domain
def delete_domain(db: Session,id: int):
db_domain = db.query(models.Domain).get(id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
#if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
if db_domain:
db.delete(db_domain)
db.commit()
return True
def edit_domain(
db: Session, domain: schemas.DomainBase,userid:int
db: Session, domain: schemas.DomainIn,userid:int
) -> schemas.Domain:
domain.encrypt_kintonepwd()
if domain.kintonepwd != "":
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db_domain.tenantid = domain.tenantid
db_domain.name=domain.name
db_domain.url=domain.url
if db_domain.is_active == True and domain.is_active == False:
db_userdomains = db.query(models.UserDomain).filter(and_(models.UserDomain.domainid == db_domain.id,models.UserDomain.active == True)).all()
for userdomain in db_userdomains:
userdomain.active = False
db.add(userdomain)
db_domain.is_active=domain.is_active
db_domain.kintoneuser=domain.kintoneuser
db_domain.kintonepwd = domain.kintonepwd
if domain.kintonepwd != "":
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db_domain.update_time = datetime.now
db_domain.ownerid = domain.ownerid
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def add_userdomain(db: Session, userid:int,domainid:int):
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
return user_domain
def add_admindomain(db: Session,userid:int,domainid:int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
if db_domain:
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
db.commit()
return db_domain
def add_userdomain(db: Session,ownerid:int, userid:int,domainid:int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.ownerid == ownerid,models.Domain.is_active)).first()
if db_domain:
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
db.commit()
return db_domain
def add_userdomains(db: Session, userid:int,domainids:list[str]):
dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
@@ -338,35 +360,40 @@ def add_userdomains(db: Session, userid:int,domainids:list[str]):
def delete_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
#if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
if db_domain:
db.delete(db_domain)
db.commit()
return True
def active_userdomain(db: Session, userid: int,domainid: int):
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
if not db_userdomains:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
return db_userdomains
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
if db_domain:
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
# if not db_userdomains:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
return db_domain
def get_activedomain(db: Session, userid: int)-> t.Optional[models.Domain]:
user_domains = (db.query(models.Domain,models.UserDomain.active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
.filter(models.UserDomain.userid == userid)
.all())
db_domain=None
if len(user_domains)==1:
db_domain = user_domains[0][0];
else:
db_domain = next((domain for domain,active in user_domains if active),None)
def get_activedomain(db: Session, userid: int):
# user_domains = (db.query(models.Domain,models.UserDomain.active)
# .join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
# .filter(models.UserDomain.userid == userid)
# .all())
db_domain=(db.query(models.Domain).filter(models.Domain.is_active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id).filter(and_(models.UserDomain.active,models.UserDomain.userid == userid)).first())
# if len(user_domains)==1:
# db_domain = user_domains[0][0];
# else:
# db_domain = next((domain for domain,active in user_domains if active),None)
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
return db_domain
@@ -379,13 +406,12 @@ def get_domain(db: Session, userid: str):
# domain.kintonepwd = decrypted_pwd
return domains
def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
def get_alldomains(db: Session):
domains = db.query(models.Domain).all()
return domains
def get_domains(db: Session,userid:int):
domains = db.query(models.Domain).filter(models.Domain.ownerid == userid ).all()
return domains
def get_events(db: Session):

View File

@@ -0,0 +1,2 @@
from app.db.cruddb.dbuser import dbuser
from app.db.cruddb.dbdomain import dbdomain

View File

@@ -0,0 +1,104 @@
from sqlalchemy import asc, desc
from sqlalchemy.orm import Session
from sqlalchemy.orm.query import Query
from typing import Type, List, Optional
from app.core.common import ApiReturnPage
from sqlalchemy import and_ ,or_
from pydantic import BaseModel
from app.db import models
class crudbase:
def __init__(self, model: Type[models.Base]):
self.model = model
def _apply_filters(self, query: Query, filters: dict) -> Query:
and_conditions = []
or_conditions = []
for column_name, value in filters.items():
column = getattr(self.model, column_name, None)
if column:
if isinstance(value, dict):
if 'operator' in value:
operator = value['operator']
filter_value = value['value']
if operator == '!=':
and_conditions.append(column != filter_value)
elif operator == 'like':
and_conditions.append(column.like(f"%{filter_value}%"))
elif operator == '=':
and_conditions.append(column == filter_value)
elif operator == '>':
and_conditions.append(column > filter_value)
elif operator == '>=':
and_conditions.append(column >= filter_value)
elif operator == '<':
and_conditions.append(column < filter_value)
elif operator == '<=':
and_conditions.append(column <= filter_value)
elif operator == 'in':
if isinstance(filter_value, list):
or_conditions.append(column.in_(filter_value))
else:
and_conditions.append(column == filter_value)
else:
and_conditions.append(column == value)
else:
and_conditions.append(column == value)
if and_conditions:
query = query.filter(*and_conditions)
if or_conditions:
query = query.filter(or_(*or_conditions))
return query
def _apply_sorting(self, query: Query, sort_by: Optional[str], sort_order: Optional[str]) -> Query:
if sort_by:
column = getattr(self.model, sort_by, None)
if column:
if sort_order == "desc":
query = query.order_by(desc(column))
else:
query = query.order_by(asc(column))
return query
def get_all(self, db: Session) -> Query:
return db.query(self.model)
def get(self, db: Session, item_id: int) -> Optional[models.Base]:
return db.query(self.model).get(item_id)
def create(self, db: Session, obj_in: BaseModel) -> models.Base:
db_obj = self.model(**obj_in.model_dump())
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, item_id: int, obj_in: BaseModel) -> Optional[models.Base]:
db_obj = db.query(self.model).filter(self.model.id == item_id).first()
if db_obj:
for key, value in obj_in.model_dump(exclude_unset=True).items():
setattr(db_obj, key, value)
db.commit()
db.refresh(db_obj)
return db_obj
return None
def delete(self, db: Session, item_id: int) -> Optional[models.Base]:
db_obj = db.query(self.model).get(item_id)
if db_obj:
db.delete(db_obj)
db.commit()
return db_obj
return None
def get_by_conditions(self, db: Session, filters: Optional[dict] = None, sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc") -> Query:
query = db.query(self.model)
if filters:
query = self._apply_filters(query, filters)
if sort_by:
query = self._apply_sorting(query, sort_by, sort_order)
print(str(query))
return query

View File

@@ -0,0 +1,139 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import and_
import typing as t
from app.db.cruddb.crudbase import crudbase
from fastapi_pagination.ext.sqlalchemy import paginate
from app.core.common import ApiReturnPage
from app.db import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
class dbuserdomain(crudbase):
def __init__(self):
super().__init__(model=models.UserDomain)
def get_userdomain(self,db: Session,userid:int,domainid:int):
return super().get_by_conditions(db,{"userid":userid,"domainid":domainid}).first()
def get_userdomain_by_domainid(self,db: Session,ownerid:int,domainid:int):
return super().get_by_conditions(db,{"domainid":domainid})
def get_default_domains(self,db: Session,domainid:int):
return super().get_by_conditions(db,{"domainid":domainid,"is_default":True}).all()
def get_user_default_domain(self,db: Session,userid:int):
return super().get_by_conditions(db,{"userid":userid,"is_default":True}).first()
dbuserdomain = dbuserdomain()
class dbdomain(crudbase):
def __init__(self):
super().__init__(model=models.Domain)
def get_domains(self,db: Session)-> ApiReturnPage[models.Base]:
return paginate(super().get_all(db))
def get_domains_by_owner(self,db: Session,ownerid:int)-> ApiReturnPage[models.Base]:
return paginate( super().get_by_conditions(db,{"ownerid":ownerid}))
def create_domain(self,db: Session, domain: schemas.DomainIn,userid:int):
#db_domain = super().get_by_conditions(db,{"url":domain.url,"kintoneuser":domain.kintoneuser,"onwerid":userid}).first()
#if not db_domain:
domain.encrypt_kintonepwd()
domain.id = None
domain.createuserid = userid
domain.updateuserid = userid
domain.ownerid = userid
return super().create(db,domain)
#return db_domain
def delete_domain(self,db: Session,id: int):
return super().delete(db,id)
def edit_domain(self,db: Session, domain: schemas.DomainIn,userid:int) -> schemas.DomainOut:
db_domain = super().get(db,domain.id)
if db_domain:
db_domain.tenantid = domain.tenantid
db_domain.name=domain.name
db_domain.url=domain.url
if db_domain.is_active == True and domain.is_active == False:
db_userdomains = dbuserdomain.get_default_domains(db,domain.id)
for userdomain in db_userdomains:
userdomain.is_default = False
db.add(userdomain)
db_domain.is_active=domain.is_active
db_domain.kintoneuser=domain.kintoneuser
if domain.kintonepwd != "" and domain.kintonepwd != None:
domain.encrypt_kintonepwd()
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
return None
def add_userdomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
db_domain = super().get_by_conditions(db,{"id":domainid,"is_active":True}).first()
if db_domain:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
db.add(user_domain)
db.commit()
return db_domain
return None
def add_userdomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
db_domain = super().get_by_conditions(db,{"id":domainid,"ownerid":ownerid,"is_active":True}).first()
if db_domain:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
db.add(user_domain)
db.commit()
return db_domain
return None
def delete_userdomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if db_userdomain:
domain = db_userdomain.domain
db.delete(db_userdomain)
db.commit()
return domain
return None
def get_default_domain(self,db: Session, userid: int) -> schemas.DomainOut:
userdomain = dbuserdomain.get_user_default_domain(db,userid)
if userdomain:
return userdomain.domain
else:
return None
def set_default_domain(self,db: Session, userid: int,domainid: int):
db_domain =super().get_by_conditions(db,{"id":domainid,"is_active":True}).first()
if db_domain:
db_default_domain = dbuserdomain.get_user_default_domain(db,userid)
db_userdomain =dbuserdomain.get_userdomain(db,userid,domainid)
if db_default_domain:
db_default_domain.is_default = False
db_default_domain.updateuserid = userid
db.add(db_default_domain)
if db_userdomain:
db_userdomain.is_default = True
db_userdomain.updateuserid = userid
db.add(db_userdomain)
db.commit()
return db_domain
def get_shareddomain_users(self,db: Session,ownerid:int,domainid: int) -> ApiReturnPage[models.Base]:
users = db.query(models.User).join(models.UserDomain,models.UserDomain.userid == models.User.id).filter(models.UserDomain.domainid ==domainid)
return paginate(users)
dbdomain = dbdomain()

View File

@@ -0,0 +1,92 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import and_
import typing as t
from app.db.cruddb.crudbase import crudbase
from fastapi_pagination.ext.sqlalchemy import paginate
from app.core.common import ApiReturnPage
from app.db import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
class dbpermission(crudbase):
def __init__(self):
super().__init__(model=models.Permission)
dbpermission = dbpermission()
class dbrole(crudbase):
def __init__(self):
super().__init__(model=models.Role)
dbrole = dbrole()
class dbuser(crudbase):
def __init__(self):
super().__init__(model=models.User)
def get_user(self,db: Session, user_id: int) -> schemas.User:
return super().get(db,user_id)
def get_user_by_email(self,db: Session, email: str) -> schemas.User:
return super().get_by_conditions(db,{"email":email}).first()
def get_users(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(super().get_all(db))
def get_users_not_admin(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(super().get_by_conditions(db,{"is_superuser":False}))
def create_user(self,db: Session, user: schemas.UserCreate,userid:int):
hashed_password = get_password_hash(user.password)
user.hashed_password = hashed_password
user.createuserid = userid
user.updateuserid = userid
del user.password
return super().create(db,user)
def delete_user(self,db: Session, user_id: int):
return super().delete(db,user_id)
def edit_user(self,db: Session, user_id:int,user: schemas.UserEdit,userid: int) -> schemas.User:
if not user.password is None and user.password != "":
user.hashed_password = get_password_hash(user.password)
del user.password
user.updateuserid = userid
return super().update(db,user_id,user)
def get_roles(self,db: Session) -> t.List[schemas.RoleBase]:
return dbrole.get_all(db).all()
def get_roles_by_level(self,db: Session,level:int) -> t.List[schemas.RoleBase]:
return dbrole.get_by_conditions(db,{"level":{"operator":">=","value":level}}).all()
def assign_userrole(self,db: Session, user_id: int, roles: t.List[int]):
db_user = super().get(db,user_id)
if db_user:
for role in db_user.roles:
db_user.roles.remove(role)
for roleid in roles:
role = dbrole.get(db,roleid)
if role:
db_user.roles.append(role)
db.commit()
db.refresh(db_user)
return db_user
def get_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]:
return dbpermission.get_all(db).all()
def get_user_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]:
permissions =[]
db_user = super().get(db,user_id)
if db_user:
for role in db_user.roles:
permissions += role.permissions
return list(set(permissions))
dbuser = dbuser()

View File

@@ -1,6 +1,5 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey,Table
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship,as_declarative
from datetime import datetime
from app.db.session import Base
from app.core.security import chacha20Decrypt
@@ -35,6 +34,10 @@ class User(Base):
hashed_password = Column(String(200), nullable=False)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
roles = relationship("Role",secondary=userrole,back_populates="users")
@@ -43,6 +46,7 @@ class Role(Base):
name = Column(String(100))
description = Column(String(255))
level = Column(Integer)
users = relationship("User",secondary=userrole,back_populates="roles")
permissions = relationship("Permission",secondary=rolepermission,back_populates="roles")
@@ -145,6 +149,7 @@ class Tenant(Base):
startdate = Column(DateTime)
enddate = Column(DateTime)
class Domain(Base):
__tablename__ = "domain"
@@ -153,20 +158,30 @@ class Domain(Base):
url = Column(String(200), nullable=False)
kintoneuser = Column(String(100), nullable=False)
kintonepwd = Column(String(100), nullable=False)
is_active = Column(Boolean, default=True)
def decrypt_kintonepwd(self):
decrypted_pwd = chacha20Decrypt(self.kintonepwd)
return decrypted_pwd
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
ownerid = Column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
owner = relationship('User',foreign_keys=[ownerid])
class UserDomain(Base):
__tablename__ = "userdomain"
userid = Column(Integer,ForeignKey("user.id"))
domainid = Column(Integer,ForeignKey("domain.id"))
active = Column(Boolean, default=False)
is_default = Column(Boolean, default=False)
createuserid = Column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id"))
domain = relationship("Domain")
user = relationship("User",foreign_keys=[userid])
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
class Event(Base):
__tablename__ = "event"

View File

@@ -15,10 +15,14 @@ class Permission(BaseModel):
function:str
privilege:str
class Role(BaseModel):
class RoleBase(BaseModel):
id: int
name:str
description:str
level:int
class RoleWithPermission(RoleBase):
permissions:t.List[Permission] = []
class UserBase(BaseModel):
@@ -27,19 +31,28 @@ class UserBase(BaseModel):
is_superuser: bool = False
first_name: str = None
last_name: str = None
roles:t.List[Role] = []
roles:t.List[RoleBase] = []
class UserOut(UserBase):
pass
class UserOut(BaseModel):
id: int
email: str
is_active: bool = True
is_superuser: bool = False
first_name: str = None
last_name: str = None
class UserCreate(UserBase):
email:str
password: str
hashed_password :str = None
first_name: str
last_name: str
is_active:bool
is_superuser:bool
createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None
class ConfigDict:
orm_mode = True
@@ -47,6 +60,8 @@ class UserCreate(UserBase):
class UserEdit(UserBase):
password: t.Optional[str] = None
hashed_password :str = None
updateuserid:t.Optional[int] = None
class ConfigDict:
orm_mode = True
@@ -143,28 +158,54 @@ class Flow(Base):
class ConfigDict:
orm_mode = True
class DomainBase(BaseModel):
class DomainIn(BaseModel):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
kintonepwd: str
kintonepwd: t.Optional[str] = None
is_active: bool
createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None
ownerid:t.Optional[int] = None
def encrypt_kintonepwd(self):
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
self.kintonepwd = encrypted_pwd
class DomainOut(BaseModel):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
is_active: bool
ownerid:int
class ConfigDict:
orm_mode = True
class UserDomain(BaseModel):
id: int
is_default: bool
domain:DomainOut
user:UserOut
class Domain(Base):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
kintonepwd: str
is_active: bool
updateuser:UserOut
owner:UserOut
class ConfigDict:
orm_mode = True
class Event(Base):
id: int
category: str

View File

@@ -1,6 +1,5 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import sessionmaker,declarative_base
from app.core import config

View File

@@ -1,5 +1,6 @@
import os
from fastapi import FastAPI, Depends
from fastapi_pagination import add_pagination
from starlette.requests import Request
import uvicorn
from app.api.api_v1.routers.kintone import kinton_router
@@ -14,14 +15,22 @@ from app import tasks
from fastapi.middleware.cors import CORSMiddleware
import logging
from app.core.apiexception import APIException, writedblog
from app.core.common import ApiReturnError
from app.db.crud import create_log
from fastapi.responses import JSONResponse
import asyncio
from contextlib import asynccontextmanager
Base.metadata.create_all(bind=engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
startup_event()
yield
app = FastAPI(
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api"
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api",lifespan=lifespan
)
origins = [
@@ -36,6 +45,8 @@ app.add_middleware(
allow_headers=["*"],
)
add_pagination(app)
# @app.middleware("http")
# async def db_session_middleware(request: Request, call_next):
# request.state.db = SessionLocal()
@@ -43,12 +54,11 @@ app.add_middleware(
# request.state.db.close()
# return response
@app.on_event("startup")
async def startup_event():
def startup_event():
log_dir="log"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
logger = logging.getLogger("uvicorn.access")
handler = logging.handlers.RotatingFileHandler(f"{log_dir}/api.log",mode="a",maxBytes = 100*1024, backupCount = 3)
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
@@ -60,7 +70,7 @@ async def api_exception_handler(request: Request, exc: APIException):
loop.run_in_executor(None,writedblog,exc)
return JSONResponse(
status_code=exc.status_code,
content={"detail": f"{exc.detail}"},
content= ApiReturnError(msg = f"{exc.detail}").model_dump(),
)
@app.get("/api/v1")

View File

@@ -0,0 +1,145 @@
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
import typing as t
from app.core import config, security
from app.db.session import Base, get_db
from app.db import models
from app.main import app
def get_test_db_url() -> str:
return f"{config.SQLALCHEMY_DATABASE_URI}"
@pytest.fixture
def test_db():
"""
Modify the db session to automatically roll back after each test.
This is to avoid tests affecting the database state of other tests.
"""
# Connect to the test database
engine = create_engine(
get_test_db_url(),
)
connection = engine.connect()
trans = connection.begin()
# Run a parent transaction that can roll back all changes
test_session_maker = sessionmaker(
autocommit=False, autoflush=False, bind=engine
)
test_session = test_session_maker()
#test_session.begin_nested()
# @event.listens_for(test_session, "after_transaction_end")
# def restart_savepoint(s, transaction):
# if transaction.nested and not transaction._parent.nested:
# s.expire_all()
# s.begin_nested()
yield test_session
# Roll back the parent transaction after the test is complete
test_session.close()
trans.rollback()
connection.close()
@pytest.fixture(scope="function")
def test_client(test_db):
"""
Get a TestClient instance that reads/write to the test database.
"""
def get_test_db():
yield test_db
app.dependency_overrides[get_db] = get_test_db
with TestClient(app) as test_client:
yield test_client
# @pytest.fixture
# def test_password() -> str:
# return "securepassword"
# def get_password_hash() -> str:
# """
# Password hashing can be expensive so a mock will be much faster
# """
# return "supersecrethash"
# @pytest.fixture
# def test_user(test_db) -> models.User:
# """
# Make a test user in the database
# """
# user = models.User(
# email="fake@email.com",
# hashed_password=get_password_hash(),
# is_active=True,
# )
# test_db.add(user)
# test_db.commit()
# return user
# @pytest.fixture
# def test_superuser(test_db) -> models.User:
# """
# Superuser for testing
# """
# user = models.User(
# email="fakeadmin@email.com",
# hashed_password=get_password_hash(),
# is_superuser=True,
# )
# test_db.add(user)
# test_db.commit()
# return user
# def verify_password_mock(first: str, second: str) -> bool:
# return True
# @pytest.fixture
# def user_token_headers(
# client: TestClient, test_user, test_password, monkeypatch
# ) -> t.Dict[str, str]:
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
# login_data = {
# "username": test_user.email,
# "password": test_password,
# }
# r = client.post("/api/token", data=login_data)
# tokens = r.json()
# a_token = tokens["access_token"]
# headers = {"Authorization": f"Bearer {a_token}"}
# return headers
# @pytest.fixture
# def superuser_token_headers(
# client: TestClient, test_superuser, test_password, monkeypatch
# ) -> t.Dict[str, str]:
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
# login_data = {
# "username": test_superuser.email,
# "password": test_password,
# }
# r = client.post("/api/token", data=login_data)
# tokens = r.json()
# a_token = tokens["access_token"]
# headers = {"Authorization": f"Bearer {a_token}"}
# return headers

View File

@@ -1,4 +1,6 @@
def test_read_main(client):
response = client.get("/api/v1")
def test_read_main(test_client):
response = test_client.get("/api/v1")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
assert response.json() == {"message": "success"}

View File

@@ -1,169 +0,0 @@
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import database_exists, create_database, drop_database
from fastapi.testclient import TestClient
import typing as t
from app.core import config, security
from app.db.session import Base, get_db
from app.db import models
from app.main import app
def get_test_db_url() -> str:
return f"{config.SQLALCHEMY_DATABASE_URI}_test"
@pytest.fixture
def test_db():
"""
Modify the db session to automatically roll back after each test.
This is to avoid tests affecting the database state of other tests.
"""
# Connect to the test database
engine = create_engine(
get_test_db_url(),
)
connection = engine.connect()
trans = connection.begin()
# Run a parent transaction that can roll back all changes
test_session_maker = sessionmaker(
autocommit=False, autoflush=False, bind=engine
)
test_session = test_session_maker()
test_session.begin_nested()
@event.listens_for(test_session, "after_transaction_end")
def restart_savepoint(s, transaction):
if transaction.nested and not transaction._parent.nested:
s.expire_all()
s.begin_nested()
yield test_session
# Roll back the parent transaction after the test is complete
test_session.close()
trans.rollback()
connection.close()
@pytest.fixture(scope="session", autouse=True)
def create_test_db():
"""
Create a test database and use it for the whole test session.
"""
test_db_url = get_test_db_url()
# Create the test database
assert not database_exists(
test_db_url
), "Test database already exists. Aborting tests."
create_database(test_db_url)
test_engine = create_engine(test_db_url)
Base.metadata.create_all(test_engine)
# Run the tests
yield
# Drop the test database
drop_database(test_db_url)
@pytest.fixture
def client(test_db):
"""
Get a TestClient instance that reads/write to the test database.
"""
def get_test_db():
yield test_db
app.dependency_overrides[get_db] = get_test_db
yield TestClient(app)
@pytest.fixture
def test_password() -> str:
return "securepassword"
def get_password_hash() -> str:
"""
Password hashing can be expensive so a mock will be much faster
"""
return "supersecrethash"
@pytest.fixture
def test_user(test_db) -> models.User:
"""
Make a test user in the database
"""
user = models.User(
email="fake@email.com",
hashed_password=get_password_hash(),
is_active=True,
)
test_db.add(user)
test_db.commit()
return user
@pytest.fixture
def test_superuser(test_db) -> models.User:
"""
Superuser for testing
"""
user = models.User(
email="fakeadmin@email.com",
hashed_password=get_password_hash(),
is_superuser=True,
)
test_db.add(user)
test_db.commit()
return user
def verify_password_mock(first: str, second: str) -> bool:
return True
@pytest.fixture
def user_token_headers(
client: TestClient, test_user, test_password, monkeypatch
) -> t.Dict[str, str]:
monkeypatch.setattr(security, "verify_password", verify_password_mock)
login_data = {
"username": test_user.email,
"password": test_password,
}
r = client.post("/api/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}
return headers
@pytest.fixture
def superuser_token_headers(
client: TestClient, test_superuser, test_password, monkeypatch
) -> t.Dict[str, str]:
monkeypatch.setattr(security, "verify_password", verify_password_mock)
login_data = {
"username": test_superuser.email,
"password": test_password,
}
r = client.post("/api/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}
return headers

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,211 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" version="24.9.2">
<diagram name="ページ1" id="oKiyF3b1qzm0IX1SNAWJ">
<mxGraphModel dx="1395" dy="1078" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Clf12EPbcqlM1huzJpPN-73" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d0cee2;strokeColor=#56517e;" vertex="1" parent="1">
<mxGeometry x="92" y="645" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-71" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="110" y="605" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-53" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffcccc;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="140" y="570" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-52" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#cdeb8b;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="170" y="530" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-51" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffcc99;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="210" y="490" width="720" height="180" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-49" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="90" y="20" width="1080" height="290" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-1" target="Clf12EPbcqlM1huzJpPN-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-1" value="タスク開始" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="120" y="215" width="90" height="50" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;dashed=1;curved=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-8" target="Clf12EPbcqlM1huzJpPN-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-47" value="DBからクロールのタスクを取得する" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Clf12EPbcqlM1huzJpPN-45">
<mxGeometry x="0.2449" y="53" relative="1" as="geometry">
<mxPoint x="17" y="-44" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-8" target="Clf12EPbcqlM1huzJpPN-78">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-8" value="DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#cce5ff;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="570" y="-110" width="100" height="70" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-9" value="&lt;div style=&quot;background-color: rgb(255, 255, 254); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; font-size: 14px; line-height: 19px; white-space: pre;&quot;&gt;&lt;span style=&quot;&quot;&gt;タスクスケジューラ&lt;/span&gt;&lt;/div&gt;" style="whiteSpace=wrap;html=1;rounded=1;fontColor=default;" vertex="1" parent="1">
<mxGeometry x="260" y="217.5" width="140" height="45" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;curved=1;startArrow=block;startFill=1;endArrow=none;endFill=0;dashed=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-15" target="Clf12EPbcqlM1huzJpPN-9">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-18" value="タスクの割り当て" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Clf12EPbcqlM1huzJpPN-17">
<mxGeometry x="0.04" relative="1" as="geometry">
<mxPoint x="-18" y="30" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=0;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;curved=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-15" target="Clf12EPbcqlM1huzJpPN-31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-15" value="&lt;b&gt;RabbitMQ&lt;/b&gt;" style="whiteSpace=wrap;html=1;rounded=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
<mxGeometry x="510" y="380" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-22" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-19" target="Clf12EPbcqlM1huzJpPN-21">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-19" value="MQコマンドの受信" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="250" y="560" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;curved=1;dashed=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-15" target="Clf12EPbcqlM1huzJpPN-19">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-21" target="Clf12EPbcqlM1huzJpPN-23">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-21" value="データ収集" style="whiteSpace=wrap;html=1;rounded=0;" vertex="1" parent="1">
<mxGeometry x="490" y="560" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;curved=1;dashed=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-23" target="Clf12EPbcqlM1huzJpPN-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-23" value="結果をMQメッセージを変換" style="whiteSpace=wrap;html=1;rounded=0;" vertex="1" parent="1">
<mxGeometry x="740" y="560" width="170" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;curved=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-26" target="Clf12EPbcqlM1huzJpPN-31">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="980" y="390" />
<mxPoint x="980" y="350" />
<mxPoint x="745" y="350" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-26" value="データ収集結果&lt;br&gt;一時保存" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#eeeeee;strokeColor=#36393d;" vertex="1" parent="1">
<mxGeometry x="970" y="390" width="100" height="70" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-27" value="タスクコマンド" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="1">
<mxGeometry x="340" y="450" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-23" target="Clf12EPbcqlM1huzJpPN-26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-29" value="コンテンツなど大きい情報を一時保存" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="950" y="500" width="230" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-30" value="収集結果通知MQ" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="740" y="450" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-37" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-31" target="Clf12EPbcqlM1huzJpPN-36">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-31" value="メッセージキューから&lt;div&gt;データ受信&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="677" y="212.5" width="150" height="50" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-34" value="MQのキーで収集結果を取得" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="760" y="350" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-36" target="Clf12EPbcqlM1huzJpPN-38">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-36" value="&lt;div style=&quot;background-color: rgb(255, 255, 254); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; white-space: pre;&quot;&gt;&lt;span&gt;データクリーニング&lt;br&gt;と重複排除&lt;/span&gt;&lt;/div&gt;" style="whiteSpace=wrap;html=1;rounded=1;fontColor=default;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="890" y="207.5" width="148" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-43" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-38" target="Clf12EPbcqlM1huzJpPN-42">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-38" value="データベースに保存" style="whiteSpace=wrap;html=1;fontSize=11;rounded=1;" vertex="1" parent="1">
<mxGeometry x="904" y="60" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-42" target="Clf12EPbcqlM1huzJpPN-31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-42" value="収集結果からサブ項目を抽出" style="whiteSpace=wrap;html=1;fontSize=11;rounded=1;" vertex="1" parent="1">
<mxGeometry x="692" y="60" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-46" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;curved=1;dashed=1;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-38" target="Clf12EPbcqlM1huzJpPN-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-48" value="クロール結果をDBに登録する" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Clf12EPbcqlM1huzJpPN-46">
<mxGeometry x="0.1149" y="13" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-50" value="&lt;b&gt;メインプログラム&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="540" y="20" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-54" value="&lt;b&gt;クローラー サブプログラム&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="485" y="490" width="185" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-55" value="&lt;b&gt;SharePointクローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="220" y="640" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-60" value="MQ情報" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;align=center;fontSize=14;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" vertex="1" parent="1">
<mxGeometry x="1024" y="630" width="160" height="236" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-61" value="MQキー" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="26" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-70" value="クローラーID" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="56" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-62" value="分類" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="86" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-63" value="タイトル" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="116" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-64" value="URL" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="146" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-65" value="サブ項目" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="176" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-66" value="コンテンツ" style="text;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=12;whiteSpace=wrap;html=1;" vertex="1" parent="Clf12EPbcqlM1huzJpPN-60">
<mxGeometry y="206" width="160" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-67" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-60" target="Clf12EPbcqlM1huzJpPN-23">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="930" y="840" as="sourcePoint" />
<mxPoint x="980" y="790" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-68" value="&lt;b&gt;OneNodeクローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="180" y="680" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-69" value="&lt;b&gt;EIM クローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="150" y="720" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-72" value="&lt;b&gt;Coredasuクローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="120" y="755" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-74" value="&lt;b&gt;その他クローラー&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="92" y="795" width="130" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-75" value="PostGre SQL" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="585" y="-40" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-76" value="再帰的な処理" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="1030" y="140" width="80" height="50" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-77" value="NEO4J" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;" vertex="1" parent="1">
<mxGeometry x="570" y="-390" width="100" height="80" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-78" value="データ抽出" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="560" y="-250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Clf12EPbcqlM1huzJpPN-80" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Clf12EPbcqlM1huzJpPN-78" target="Clf12EPbcqlM1huzJpPN-77">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,6 +1,6 @@
{
"name": "k-tune",
"version": "0.2.0",
"version": "2.0.0 Beta",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "k-tune | kintoneジェネレーター",
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",

View File

@@ -115,7 +115,8 @@ module.exports = configure(function (/* ctx */) {
// Quasar plugins
plugins: [
'Notify'
'Notify',
'Dialog'
]
},

View File

@@ -1,64 +1,58 @@
<template>
<div class="q-px-xs">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else class="app-table" :selection="type" row-key="id" v-model:selected="selected" flat bordered
virtual-scroll :columns="columns" :rows="rows" :pagination="pagination" :rows-per-page-options="[0]"
:filter="filter" style="max-height: 65vh;">
<template v-slot:body-cell-description="props">
<q-td :props="props">
<q-scroll-area class="description-cell">
<div v-html="props.row.description"></div>
</q-scroll-area>
</q-td>
</template>
</q-table>
</div>
<detail-field-table
detailField="description"
:name="name"
:type="type"
:filter="filter"
:columns="columns"
:fetchData="fetchApps"
@update:selected="(item) => { selected = item }"
/>
</template>
<script lang="ts">
import { ref, onMounted, reactive, watchEffect } from 'vue'
import { ref, PropType } from 'vue';
import { api } from 'boot/axios';
import DetailFieldTable from './dialog/DetailFieldTable.vue';
interface IAppDisplay {
id: string;
name: string;
description: string;
createdate: string;
}
export default {
name: 'AppSelectBox',
components: {
DetailFieldTable
},
props: {
name: String,
type: String,
filter: String,
updateSelectApp: {
type: Function
filterInitRowsFunc: {
type: Function as PropType<(app: IAppDisplay) => boolean>,
}
},
setup(props) {
const selected = ref<IAppDisplay[]>([]);
const columns = [
{ name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true },
{ name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true, sort: (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10) },
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' },
{ name: 'description', label: '概要', field: 'description', align: 'left', sortable: false },
{ name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' }
]
const isLoaded = ref(false);
const rows: any[] = reactive([]);
const selected = ref([])
];
watchEffect(()=>{
if (selected.value && selected.value[0] && props.updateSelectApp) {
props.updateSelectApp(selected.value[0])
}
});
onMounted(() => {
api.get('api/v1/allapps').then(res => {
res.data.apps.forEach((item: any) => {
rows.push({
id: item.appId,
name: item.name,
description: item.description,
createdate: dateFormat(item.createdAt)
});
});
isLoaded.value = true;
});
});
const fetchApps = async () => {
const res = await api.get('api/v1/allapps');
return res.data.apps.map((item: any) => ({
id: item.appId,
name: item.name,
description: item.description,
createdate: dateFormat(item.createdAt)
})).filter(app => !props.filterInitRowsFunc || props.filterInitRowsFunc(app));
};
const dateFormat = (dateStr: string) => {
const date = new Date(dateStr);
@@ -70,31 +64,13 @@ export default {
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
}
};
return {
columns,
rows,
selected,
isLoaded,
pagination: ref({
rowsPerPage: 10
})
}
},
}
</script>
<style lang="scss">
.description-cell {
height: 60px;
width: 300px;
max-height: 60px;
max-width: 300px;
white-space: break-spaces;
}
.spinner {
min-height: 300px;
min-width: 400px;
}
</style>
fetchApps,
selected
};
}
};
</script>

View File

@@ -1,37 +1,78 @@
<template>
<div class="q-pa-md">
<q-table :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
<q-table :loading="loading" :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows">
<template v-slot:body-cell-name="p">
<q-td class="content-box flex justify-between items-center" :props="p">
{{ p.row.name }}
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
<q-badge v-if="p.row.id == currentDomainId" color="primary">現在</q-badge>
</q-td>
</template>
</q-table>
</div>
</template>
<script>
import { ref,onMounted,reactive } from 'vue'
import { ref,onMounted,reactive, computed } from 'vue'
import { api } from 'boot/axios';
import { useAuthStore } from 'src/stores/useAuthStore';
export default {
name: 'DomainSelect',
props: {
name: String,
type: String
type: String,
filterInitRowsFunc: {
type: Function,
},
},
setup() {
const columns = [
{ name: 'id'},
{ name: 'tenantid', required: true,label: 'テナント',align: 'left',field: 'tenantid',sortable: true},
{ name: 'name', align: 'center', label: 'ドメイン', field: 'name', sortable: true },
{ name: 'url', label: 'URL', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'アカウント', field: 'kintoneuser' }
]
const rows = reactive([])
onMounted( () => {
api.get(`api/domains/1`).then(res =>{
res.data.forEach((item) =>
{
rows.push({id:item.id,tenantid:item.tenantid,name:item.name,url:item.url,kintoneuser:item.kintoneuser});
}
)
});
setup(props) {
const authStore = useAuthStore();
const currentDomainId = computed(() => authStore.currentDomain.id);
const loading = ref(true);
const inactiveRowClass = (row) => row.domainActive ? '' : 'inactive-row';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'name', align: 'left', label: 'ドメイン', field: 'name', sortable: true, classes: inactiveRowClass },
{ name: 'url', label: 'URL', field: 'url',align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'user', label: 'アカウント', field: 'user',align: 'left', classes: inactiveRowClass },
{ name: 'owner', label: '所有者', field: row => row.owner.fullName, align: 'left', classes: inactiveRowClass },
]
const rows = reactive([]);
onMounted(() => {
loading.value = true;
api.get(`api/domains`).then(res =>{
res.data.data.forEach((data) => {
const item = {
id: data.id,
tenantid: data.tenantid,
domainActive: data.is_active,
name: data.name,
url: data.url,
user: data.kintoneuser,
owner: {
id: data.owner.id,
firstName: data.owner.first_name,
lastName: data.owner.last_name,
fullNameSearch: (data.owner.last_name + data.owner.first_name).toLowerCase(),
fullName: data.owner.last_name + ' ' + data.owner.first_name,
email: data.owner.email,
isActive: data.owner.is_active,
isSuperuser: data.owner.is_superuser,
}
}
if (props.filterInitRowsFunc && !props.filterInitRowsFunc(item)) {
return;
}
rows.push(item);
})
loading.value = false;
});
});
return {
loading,
currentDomainId,
columns,
rows,
selected: ref([]),
@@ -40,3 +81,11 @@ export default {
}
</script>
<style lang="scss">
.q-table td.inactive-row {
color: #aaa;
}
.q-table .content-box {
box-sizing: content-box;
}
</style>

View File

@@ -7,15 +7,15 @@
icon="share"
size="md"
:label="userStore.currentDomain.domainName"
:disable-dropdown="isUnclickable"
:dropdown-icon="isUnclickable ? 'none' : ''"
:disable="isUnclickable"
:disable-dropdown="true"
dropdown-icon='none'
:disable="true"
>
<q-list>
<q-item v-for="domain in domains" :key="domain.domainName"
<q-item :active="isCurrentDomain(domain)" active-class="active-domain-item" v-for="domain in domains" :key="domain.domainName"
clickable v-close-popup @click="onItemClick(domain)">
<q-item-section side>
<q-icon name="share" size="sm" color="orange" text-color="white"></q-icon>
<q-icon name="share" size="sm" :color="isCurrentDomain(domain) ? 'orange': ''" text-color="white"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{{domain.domainName}}</q-item-label>
@@ -27,21 +27,28 @@
</template>
<script setup lang="ts" >
import { IDomainInfo } from 'src/types/ActionTypes';
import { useAuthStore,IUserState } from 'stores/useAuthStore';
import { ref, computed } from 'vue';
import { IDomainInfo } from 'src/types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore';
import { useDomainStore } from 'src/stores/useDomainStore';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useAuthStore();
const domainStore = useDomainStore();
const route = useRoute()
const domains = ref<IDomainInfo[]>([]);
const domains = computed(() => domainStore.userDomains);
(async ()=>{
domains.value = await userStore.getUserDomains();
await domainStore.loadUserDomains();
})();
const isUnclickable = computed(()=>{
return route.path.startsWith('/FlowChart/') || domains.value === undefined || domains.value.length === 0;
});
const isCurrentDomain=(domain:IDomainInfo)=>{
return domain.id === userStore.currentDomain.id;
}
const onItemClick=(domain:IDomainInfo)=>{
console.log(domain);
@@ -54,6 +61,11 @@ const onItemClick=(domain:IDomainInfo)=>{
cursor: default !important;
}
.q-item.active-domain-item {
color: inherit;
background: #eee;
}
.q-btn.disabled.customized-disabled-btn * {
cursor: default !important;
}

View File

@@ -4,6 +4,7 @@
tag="a"
:target="target?target:'_blank'"
:href="link"
:disable="disable"
v-if="!isSeparator"
>
<q-item-section
@@ -33,6 +34,7 @@ export interface EssentialLinkProps {
icon?: string;
isSeparator?: boolean;
target?:string;
disable?:boolean;
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',

View File

@@ -0,0 +1,191 @@
<template>
<q-dialog :auto-close="false" :model-value="visible" persistent bordered>
<q-card class="dialog-content" >
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{domain.name}}のドメイン利用権限設定</q-toolbar-title>
<q-btn flat round dense icon="close" @click="close" />
</q-toolbar>
<q-card-section class="q-mx-md " >
<q-select
class="q-mt-md"
:disable="loading||!domain.domainActive"
filled
dense
v-model="canSharedUserFilter"
use-input
input-debounce="0"
:options="canSharedUserFilteredOptions"
clearable
:placeholder="canSharedUserFilter ? '' : domain.domainActive ? '権限を付与するユーザーを選択' : 'ドメインが無効なため、権限を付与できません'"
@filter="filterFn"
:display-value="canSharedUserFilter?`${canSharedUserFilter.fullName} ${canSharedUserFilter.email}`:''">
<template v-slot:after>
<q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="付与" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplay)" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>{{scope.opt.id}}</q-item-section>
<q-item-section>{{scope.opt.fullName}}</q-item-section>
<q-item-section>{{scope.opt.email}}</q-item-section>
</q-item>
</template>
</q-select>
<sharing-user-list class="q-mt-md" style="height: 330px" :users="sharedUsers" :loading="loading" title="ドメイン利用権限を持つユーザー">
<template v-slot:actions="{ row }">
<q-btn title="解除" flat color="primary" padding="xs" size="1em" :loading="row.id == removingUser?.id" icon="person_off" @click="removeShareTo(row)" />
</template>
</sharing-user-list>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" @click="close" />
<q-btn flat label="キャンセル" @click="close" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios';
import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
interface Props {
modelValue: boolean;
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const addLoading = ref(false);
const removingUser = ref<IUserDisplay>();
const loading = ref(true);
const visible = ref(props.modelValue);
const allUsers = ref<IUserDisplay[]>([]);
const sharedUsers = ref<IUserDisplay[]>([]);
const sharedUsersIdSet = new Set<number>();
const canSharedUsers = ref<IUserDisplay[]>([]);
const canSharedUserFilter = ref<IUserDisplay>();
const canSharedUserFilteredOptions = ref<IUserDisplay[]>([]);
const filterFn = (val:string, update: (cb: () => void) => void) => {
update(() => {
if (val === '') {
canSharedUserFilteredOptions.value = canSharedUsers.value;
return;
}
const needle = val.toLowerCase();
canSharedUserFilteredOptions.value = canSharedUsers.value.filter(v =>
v.email.toLowerCase().indexOf(needle) > -1 || v.fullNameSearch.toLowerCase().indexOf(needle) > -1);
})
}
watch(
() => props.modelValue,
async (newValue) => {
visible.value = newValue;
sharedUsers.value = [];
canSharedUserFilter.value = undefined
if (newValue) {
await loadShared();
}
}
);
watch(
() => visible.value,
(newValue) => {
emit('update:modelValue', newValue);
}
);
const close = () => {
emit('close');
};
const shareTo = async (user: IUserDisplay) => {
addLoading.value = true;
loading.value = true;
await api.post(`api/domain/${user.id}?domainid=${props.domain.id}`)
await loadShared();
canSharedUserFilter.value = undefined;
loading.value = false;
addLoading.value = false;
}
const removeShareTo = async (user: IUserDisplay) => {
removingUser.value = user;
loading.value = true;
await api.delete(`api/domain/${props.domain.id}/${user.id}`)
await loadShared();
loading.value = false;
removingUser.value = undefined;
};
const loadShared = async () => {
loading.value = true;
sharedUsersIdSet.clear();
const { data } = await api.get(`/api/domainshareduser/${props.domain.id}`);
sharedUsers.value = data.data.map((item: IUser) => {
const val = itemToDisplay(item);
sharedUsersIdSet.add(val.id);
return val;
});
canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id));
canSharedUserFilteredOptions.value = canSharedUsers.value;
loading.value = false;
}
onMounted(async () => {
await getUsers();
})
const getUsers = async () => {
if (Object.keys(allUsers.value).length > 0) {
return;
}
loading.value = true;
const result = await api.get(`api/v1/users`);
allUsers.value = result.data.data.map(itemToDisplay);
loading.value = false;
}
const itemToDisplay = (item: IUser) => {
return {
id: item.id,
firstName: item.first_name,
lastName: item.last_name,
fullNameSearch: (item.last_name + item.first_name).toLowerCase(),
fullName: item.last_name + ' ' + item.first_name,
email: item.email,
isSuperuser: item.is_superuser,
isActive: item.is_active,
} as IUserDisplay
}
</script>
<style lang="scss">
.dialog-content {
width: 60vw;
max-height: 80vh;
.q-select {
min-width: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<q-table :rows="users" :filter="filter" dense :columns="columns" row-key="id" :loading="loading" :pagination="pagination">
<template v-slot:top>
<div class="h6 text-weight-bold">{{props.title}}</div>
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props">
<slot name="actions" :row="props.row"></slot>
</q-td>
</template>
</q-table>
</template>
<script setup lang="ts">
import { ref, PropType } from 'vue';
import { IUserDisplay } from '../../types/UserTypes';
const props = defineProps({
users: {
type: Array as PropType<IUserDisplay[]>,
required: true,
},
loading: {
type: Boolean,
default: false,
},
title: String
});
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'actions', label: '', field: 'actions', sortable: false },
];
const filter = ref('');
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 10 });
</script>

View File

@@ -4,7 +4,7 @@
<q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle">
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ name }}</q-toolbar-title>
<q-space></q-space>
<q-space v-if="$slots.toolbar"></q-space>
<slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" />
</q-toolbar>
@@ -12,8 +12,8 @@
<slot></slot>
</q-card-section>
<q-card-actions v-if="!disableBtn" align="right" class="text-primary">
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
<q-btn flat label="確定" :loading="okBtnLoading" :v-close-popup="okBtnAutoClose" @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" :disable="okBtnLoading" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions>
</q-card>
</q-dialog>
@@ -30,13 +30,19 @@ export default {
height:String,
minWidth:String,
minHeight:String,
okBtnLoading:Boolean,
okBtnAutoClose:{
type: Boolean,
default: true
},
disableBtn:{
type: Boolean,
default: false
}
},
emits: [
'close'
'close',
'update:visible'
],
setup(props, context) {
const CloseDialogue = (val) => {

View File

@@ -0,0 +1,65 @@
<template>
<q-btn flat padding="xs" round size="1em" icon="more_vert" class="action-menu">
<q-menu>
<q-list dense :style="'min-width:' + minWidth ">
<template v-for="(item, index) in actions" :key="index" >
<q-item v-if="isAction(item)" :class="item.class" clickable v-close-popup @click="item.action(row)">
<q-item-section side style="color: inherit;">
<q-icon size="1.2em" :name="item.icon" />
</q-item-section>
<q-item-section>{{ item.label }}</q-item-section>
</q-item>
<q-separator v-else />
</template>
</q-list>
</q-menu>
</q-btn>
</template>
<script lang="ts">
import { PropType } from 'vue';
import { IDomainOwnerDisplay } from '../types/DomainTypes';
interface Action {
label: string;
icon?: string;
action: (row: any) => void|Promise<void>;
class?: string;
}
interface Separator {
separator: boolean;
}
type MenuItem = Action | Separator;
export default {
name: 'TableActionMenu',
props: {
row: {
type: Object as PropType<IDomainOwnerDisplay>,
required: true
},
minWidth: {
type: String,
default: '100px'
},
actions: {
type: Array as PropType<MenuItem[]>,
required: true
}
},
methods: {
isAction(item: MenuItem): item is Action {
return !('separator' in item);
}
}
};
</script>
<style lang="scss" scoped>
.q-table tr > td:last-child .action-menu {
opacity: 0.25;
}
.q-table tr:hover > td:last-child .action-menu {
opacity: 1;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<q-card :class="['domain-card', item.id == activeId ? 'default': '']">
<q-card-section>
<div class="row no-wrap">
<div class="col">
<div class="text-h6 ellipsis">{{ item.name }}</div>
<div class="text-subtitle2">{{ item.url }}</div>
</div>
<div v-if="!isOwnerFunc(item.owner.id)" class="col-auto">
<!-- <q-badge color="secondary" text-color="white" align="middle" class="q-mb-xs" label="他人の所有" /> -->
<q-chip square color="secondary" text-color="white" icon="people" label="他人の所有" size="sm" />
</div>
</div>
</q-card-section>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-grey-7 text-caption text-weight-medium">
アカウント
</div>
<div class="smaller-font-size">{{ item.user }}</div>
</div>
<div class="col-auto">
<div class="text-grey-7 text-caption text-weight-medium">
所有者
</div>
<div class="smaller-font-size">{{ item.owner.fullName }}</div>
</div>
</div>
</q-card-section>
<q-separator v-if="$slots.actions" />
<slot name="actions" :item="item"></slot>
</q-card>
</template>
<script setup lang="ts">
import { defineProps, computed } from 'vue';
import { IDomainOwnerDisplay } from 'src/types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore';
const props = defineProps<{
item: IDomainOwnerDisplay;
activeId: number;
}>();
const authStore = useAuthStore();
const isOwnerFunc = computed(() => (ownerId: string) => {
return ownerId == authStore.userId;
});
</script>
<style lang="scss" scoped>
.domain-card.default {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
0 10px 14px 1px rgba(0, 0, 0, 0.14),
0 4px 18px 3px rgba(0, 0, 0, 0.12),
inset 0 0 0px 2px #1976D2;
}
.domain-card {
width: 22rem;
word-break: break-word;
.smaller-font-size {
font-size: 13px;
}
}
</style>

View File

@@ -9,8 +9,8 @@ import { api } from 'boot/axios';
const props = defineProps<{filter:string}>()
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'firstName', label: '氏名', field: 'firstName', align: 'left', sortable: true },
{ name: 'lastName', label: '苗字', field: 'lastName', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
];
@@ -24,7 +24,7 @@ defineExpose({
const getUsers = async (filter = () => true) => {
loading.value = true;
const result = await api.get(`api/v1/users`);
rows.value = result.data.map((item) => {
rows.value = result.data.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
}).filter(filter);
loading.value = false;

View File

@@ -0,0 +1,88 @@
<template>
<div class="q-px-xs">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else class="app-table" :selection="type" row-key="id" v-model:selected="selected" flat bordered
virtual-scroll :columns="columns" :rows="rows" :pagination="pagination" :rows-per-page-options="[0]"
:filter="filter" style="max-height: 65vh;" @update:selected="emitSelected">
<template v-slot:[`body-cell-${detailField}`]="props">
<q-td :props="props">
<q-scroll-area class="description-cell">
<div v-html="props.row[detailField]"></div>
</q-scroll-area>
</q-td>
</template>
</q-table>
</div>
</template>
<script lang="ts">
import { ref, onMounted, reactive, PropType } from 'vue'
interface IRow {
[key: string]: any;
}
export default {
name: 'DetailFieldTable',
props: {
name: String,
type: String,
filter: String,
detailField: {
type: String,
required: true
},
columns: {
type: Array as PropType<any[]>,
required: true
},
fetchData: {
type: Function as PropType<() => Promise<IRow[]>>,
required: true
}
},
emits: ['update:selected'],
setup(props, { emit }) {
const isLoaded = ref(false);
const rows = reactive<IRow[]>([]);
const selected = ref([]);
onMounted(async () => {
const data = await props.fetchData();
rows.push(...data);
isLoaded.value = true;
});
const emitSelected = (selectedItems: any[]) => {
emit('update:selected', selectedItems);
};
return {
rows,
selected,
isLoaded,
pagination: ref({
rowsPerPage: 10
}),
emitSelected
};
}
};
</script>
<style lang="scss">
.description-cell {
height: 60px;
width: 300px;
max-height: 60px;
max-width: 300px;
white-space: break-spaces;
}
.spinner {
min-height: 300px;
min-width: 400px;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<detail-field-table
detailField="comment"
type="single"
:columns="columns"
:fetchData="fetchVersionHistory"
@update:selected="(item) => { selected = item }"
/>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue';
import { IAppDisplay, IAppVersion } from 'src/types/AppTypes';
import { date } from 'quasar';
import { api } from 'boot/axios';
import DetailFieldTable from './DetailFieldTable.vue';
import { IUser, IUserDisplay } from 'src/types/UserTypes';
export default defineComponent({
name: 'VersionHistory',
components: {
DetailFieldTable
},
props: {
app: {
type: Object as PropType<IAppDisplay>,
required: true,
},
},
setup(props, { emit }) {
const selected = ref<IAppVersion[]>([]);
const columns = [
{ name: 'version', label: 'ID', field: 'version', align: 'left', sortable: true },
{ name: 'name', label: 'バージョン名', field: 'name', align: 'left', sortable: true },
{ name: 'comment', label: 'コメント', field: 'comment', align: 'left', sortable: true },
{ name: 'creator', label: '作成者', field: (row: IAppVersion) => row.creator.fullName, align: 'left', sortable: true },
{ name: 'createTime', label: '作成日時', field: 'createTime', align: 'left', sortable: true },
{ name: 'updater', label: '更新者', field: (row: IAppVersion) => row.updater.fullName, align: 'left', sortable: true },
{ name: 'updateTime', label: '更新日時', field: 'updateTime', align: 'left', sortable: true },
];
const formatDate = (dateStr: string) => {
return date.formatDate(dateStr, 'YYYY/MM/DD HH:mm:ss');
};
const toUserDisaplay = (user: IUser) => {
return {
id: user.id,
firstName: user.first_name,
lastName: user.last_name,
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
fullName: user.last_name + ' ' + user.first_name,
email: user.email,
isActive: user.is_active,
isSuperuser: user.is_superuser,
}
}
const fetchVersionHistory = async () => {
const { data } = await api.get(`api/apps/${props.app.id}/versions`);
return data.data.map((item: any) => ({
id: item.id,
version: item.version,
appid: item.appid,
name: item.name,
comment: item.comment,
updater: toUserDisaplay(item.updateuser),
updateTime: formatDate(item.updatetime),
creator: toUserDisaplay(item.createuser),
createTime: formatDate(item.createtime),
} as IAppVersion));
};
return {
fetchVersionHistory,
columns,
selected
};
},
});
</script>

View File

@@ -0,0 +1,42 @@
<template>
<q-input
v-model="versionInfo.name"
filled
autofocus
label="バージョン名"
:rules="[(val) => !val || val.length <= 30 || '30字以内で入力ください']"
/>
<q-input
v-model="versionInfo.desc"
filled
type="textarea"
:rules="[(val) => !val || val.length <= 80 || '80字以内で入力ください']"
label="説明"
/>
</template>
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from 'vue';
import { QInput } from 'quasar';
import { IVersionInfo } from 'src/types/AppTypes';
const props = defineProps<{
modelValue: IVersionInfo;
}>();
const defaultTitle = `${new Date().toLocaleString()}`;
const versionInfo = ref({
...props.modelValue,
name: props.modelValue.name || defaultTitle,
});
const emit = defineEmits(['update:modelValue']);
watch(
versionInfo,
() => {
emit('update:modelValue', { ...versionInfo.value });
},
{ immediate: true, deep: true }
);
</script>

View File

@@ -0,0 +1,33 @@
<template>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell-name="p">
<q-td class="flex justify-between items-center" :props="p">
{{ p.row.name }}
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
<q-badge v-if="p.row.id == currendDomainId" color="primary">現在</q-badge>
</q-td>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td>
</template>
</q-table>
</template>

View File

@@ -34,20 +34,24 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { computed, onMounted, reactive } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue';
import { useAuthStore } from 'stores/useAuthStore';
import { useRoute } from 'vue-router';
const authStore = useAuthStore();
const route = useRoute()
const noDomain = computed(() => !authStore.hasDomain);
const essentialLinks: EssentialLinkProps[] = [
const essentialLinks: EssentialLinkProps[] = reactive([
{
title: 'ホーム',
caption: '設計書から導入する',
icon: 'home',
link: '/',
target: '_self'
target: '_self',
disable: noDomain
},
// {
// title: 'フローエディター',
@@ -61,7 +65,8 @@ const essentialLinks: EssentialLinkProps[] = [
caption: 'アプリを管理する',
icon: 'widgets',
link: '/#/app',
target: '_self'
target: '_self',
disable: noDomain
},
// {
// title: '条件エディター',
@@ -92,9 +97,9 @@ const essentialLinks: EssentialLinkProps[] = [
// link:'https://cybozu.dev/ja/kintone/docs/',
// icon:'help_outline'
// },
];
]);
const domainLinks: EssentialLinkProps[] = [
const domainLinks: EssentialLinkProps[] = reactive([
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
@@ -102,16 +107,16 @@ const domainLinks: EssentialLinkProps[] = [
link: '/#/domain',
target: '_self'
},
// {
// title: 'ドメイン適用',
// caption: 'ユーザー使用可能なドメインの設定',
// icon: 'assignment_ind',
// link: '/#/userDomain',
// target: '_self'
// },
];
{
title: 'ドメイン適用',
caption: 'ユーザー使用可能なドメインの設定',
icon: 'assignment_ind',
link: '/#/userDomain',
target: '_self'
},
]);
const adminLinks: EssentialLinkProps[] = [
const adminLinks: EssentialLinkProps[] = reactive([
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
@@ -119,13 +124,13 @@ const adminLinks: EssentialLinkProps[] = [
link: '/#/user',
target: '_self'
},
]
])
const version = process.env.version;
const productName = process.env.productName;
onMounted(() => {
authStore.toggleLeftMenu();
authStore.setLeftMenu(!route.path.startsWith('/FlowChart/'));
});
function toggleLeftDrawer() {

View File

@@ -8,7 +8,7 @@
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn disabled color="primary" :disable="loading" label="新規" @click="addRow" />
<q-btn color="primary" :disable="loading" label="新規" @click="showAddAppDialog" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
@@ -16,6 +16,11 @@
</template>
</q-input>
</template>
<template v-slot:body-cell-name="prop">
<q-td :props="prop">
<q-btn flat dense :label="prop.row.name" @click="toEditFlowPage(prop.row)" ></q-btn>
</q-td>
</template>
<template v-slot:body-cell-url="prop">
<q-td :props="prop">
<a :href="prop.row.url" target="_blank" :title="prop.row.name" >
@@ -25,36 +30,42 @@
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editFlow(p.row)" />
<q-btn disabled flat color="primary" padding="xs" size="1em" icon="history" @click="showHistory(p.row)" />
<q-btn disabled flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
<table-action-menu :row="p.row" :actions="actionList" />
</q-td>
</template>
</q-table>
<show-dialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeSelectAppDialog" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="dgFilter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<app-select-box ref="appDialog" name="アプリ" type="single" :filter="dgFilter" :filterInitRowsFunc="filterInitRows" />
</show-dialog>
<show-dialog v-model:visible="showVersionHistory" :name="targetRow?.name + 'のバージョン履歴'" @close="closeHistoryDg" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding">
<version-history ref="versionDialog" :app="targetRow as IAppDisplay" />
</show-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, reactive } from 'vue';
import { useQuasar } from 'quasar'
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { useFlowEditorStore } from 'stores/flowEditor';
import { router } from 'src/router';
import { date } from 'quasar'
import { IManagedApp } from 'src/types/AppTypes';
interface IAppDisplay{
id:string;
name:string;
url:string;
user:string;
version:string;
updatetime:string;
}
import { IManagedApp, IAppDisplay, IAppVersion } from 'src/types/AppTypes';
import ShowDialog from 'src/components/ShowDialog.vue';
import AppSelectBox from 'src/components/AppSelectBox.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import VersionHistory from 'components/dialog/VersionHistory.vue';
const authStore = useAuthStore();
const numberStringSorting = (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10);
@@ -63,36 +74,55 @@ const columns = [
{ name: 'id', label: 'アプリID', field: 'id', align: 'left', sortable: true, sort: numberStringSorting },
{ name: 'name', label: 'アプリ名', field: 'name', align: 'left', sortable: true },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'user', label: '最後更新者', field: 'user', align: 'left', sortable: true},
{ name: 'updatetime', label: '最後更新日', field: 'updatetime', align: 'left', sortable: true},
{ name: 'version', label: 'バージョン', field: 'version', align: 'left', sortable: true},
{ name: 'actions', label: '操作', field: 'actions' }
{ name: 'updateUser', label: '最後更新者', field: (row: IAppDisplay) => row.updateUser.fullName, align: 'left', sortable: true},
{ name: 'updateTime', label: '最後更新日', field: 'updateTime', align: 'left', sortable: true},
{ name: 'version', label: 'バージョン', field: 'version', align: 'left', sortable: true, sort: numberStringSorting },
{ name: 'actions', label: '', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const filter = ref('');
const dgFilter = ref('');
const rows = ref<IAppDisplay[]>([]);
const targetRow = ref<IAppDisplay>();
const rowIds = new Set<string>();
const $q = useQuasar()
const store = useFlowEditorStore();
const appDialog = ref();
const showSelectApp=ref(false);
const showVersionHistory=ref(false);
const isAdding = ref(false);
const actionList = [
{ label: '設定', icon: 'account_tree', action: toEditFlowPage },
{ label: '履歴', icon: 'history', action: showHistory },
{ separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
];
const getApps = async () => {
loading.value = true;
const result = await api.get('api/apps');
rows.value = result.data.map((item: IManagedApp) => {
return {
id: item.appid,
name: item.appname,
url: `${item.domainurl}/k/${item.appid}`,
user: `${item.updateuser.first_name} ${item.updateuser.last_name}` ,
updatetime:date.formatDate(item.update_time, 'YYYY/MM/DD HH:mm'),
version: Number(item.version)
}
}).sort((a: IAppDisplay, b: IAppDisplay) => numberStringSorting(a.id, b.id)); // set default order
loading.value = false;
rowIds.clear();
try {
const { data } = await api.get('api/apps');
rows.value = data.data.map((item: IManagedApp) => {
rowIds.add(item.appid);
return appToAppDisplay(item)
}).sort((a: IAppDisplay, b: IAppDisplay) => a.sortId - b.sortId); // set default order
} catch (error) {
$q.notify({
icon: 'error',
color: 'negative',
message: 'アプリ一覧の読み込みに失敗しました'
});
} finally {
loading.value = false;
}
}
onMounted(async () => {
authStore.setLeftMenu(false);
await getApps();
});
@@ -100,24 +130,74 @@ watch(() => authStore.currentDomain.id, async () => {
await getApps();
});
const addRow = () => {
const filterInitRows = (row: {id: string}) => {
return !rowIds.has(row.id);
}
const showAddAppDialog = () => {
showSelectApp.value = true;
dgFilter.value = ''
}
const closeSelectAppDialog = async (val: 'OK'|'Cancel') => {
showSelectApp.value = true;
if (val == 'OK' && appDialog.value.selected[0]) {
isAdding.value = true;
toEditFlowPage(appDialog.value.selected[0]);
}
showSelectApp.value = false;
isAdding.value = false;
}
function removeRow(app:IAppDisplay) {
targetRow.value = app;
return
}
const removeRow = (app:IAppDisplay) => {
return
function showHistory(app:IAppDisplay) {
targetRow.value = app;
showVersionHistory.value = true;
dgFilter.value = ''
}
const showHistory = (app:IAppDisplay) => {
return
const closeHistoryDg = async (val: 'OK'|'Cancel') => {
showSelectApp.value = true;
if (val == 'OK' && appDialog.value.selected[0]) {
isAdding.value = true;
await getApps();
}
showSelectApp.value = false;
isAdding.value = false;
}
const editFlow = (app:IAppDisplay) => {
const appToAppDisplay = (app: IManagedApp) => {
const user = app.updateuser;
return {
id: app.appid,
sortId: parseInt(app.appid, 10),
name: app.appname,
url: `${app.domainurl}/k/${app.appid}`,
version: app.version,
updateTime:date.formatDate(app.update_time, 'YYYY/MM/DD HH:mm'),
updateUser: {
id: user.id,
firstName: user.first_name,
lastName: user.last_name,
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
fullName: user.last_name + ' ' + user.first_name,
email: user.email,
isSuperuser: user.is_superuser,
isActive: user.is_active,
}
} as IAppDisplay
}
async function toEditFlowPage(app:IAppDisplay) {
store.setApp({
appId: app.id,
name: app.name
});
store.selectFlow(undefined);
router.push('/FlowChart/' + app.id);
await router.push('/FlowChart/' + app.id);
};
</script>

View File

@@ -14,6 +14,15 @@
<q-space></q-space>
<q-btn-dropdown color="primary" label="保存" icon="save" :loading="saveLoading" >
<q-list>
<q-item clickable v-close-popup @click="onSaveVersion">
<q-item-section avatar >
<q-icon name="bookmark_border"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>新バージョン保存</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onSaveFlow">
<q-item-section avatar >
<q-icon name="save" color="primary"></q-icon>
@@ -75,6 +84,10 @@
</template>
<action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select>
</ShowDialog>
<!-- save version dialog -->
<ShowDialog v-model:visible="saveVersionAction" name="新バージョン保存" @close="closeSaveVersionDg" min-width="500px">
<version-input v-model="versionInfo" />
</ShowDialog>
<q-inner-loading
:showing="initLoading"
color="primary"
@@ -87,7 +100,7 @@
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes';
import { IManagedApp } from 'src/types/AppTypes';
import { IAppDisplay, IManagedApp, IVersionInfo } from 'src/types/AppTypes';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore';
@@ -98,6 +111,7 @@ import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue';
import EventTree from 'components/left/EventTree.vue';
import VersionInput from 'components/dialog/VersionInput.vue';
import { FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar';
@@ -105,6 +119,7 @@ const deployLoading = ref(false);
const saveLoading = ref(false);
const initLoading = ref(true);
const drawerLeft = ref(false);
const versionInfo = ref<IVersionInfo>();
const $q = useQuasar();
const store = useFlowEditorStore();
const authStore = useAuthStore();
@@ -117,6 +132,7 @@ const prevNodeIfo = ref({
});
// const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false);
const saveVersionAction = ref(false);
const drawerRight = ref(false);
const filter=ref("");
const model = ref("");
@@ -177,7 +193,7 @@ const onDeleteAllNextNodes = (node: IActionNode) => {
}
const closeDg = (val: any) => {
console.log("Dialog closed->", val);
if (val == 'OK') {
if (val == 'OK' && appDg?.value?.selected?.length > 0) {
const data = appDg.value.selected[0];
const actionProps = JSON.parse(data.property);
const outputPoint = JSON.parse(data.outputPoints);
@@ -245,6 +261,20 @@ const onSaveActionProps=(props:IActionProperty[])=>{
}
};
const onSaveVersion = async () => {
versionInfo.value = {
id: '1' // TODO
}
saveVersionAction.value = true;
// await onSaveAllFlow();
}
const closeSaveVersionDg = (val: 'OK'|'CANCEL') => {
if (val == 'OK') {
console.log(versionInfo.value);
}
}
const onSaveFlow = async () => {
const targetFlow = store.selectedFlow;
if (targetFlow === undefined) {
@@ -316,11 +346,9 @@ const onSaveAllFlow= async ()=>{
const fetchData = async () => {
initLoading.value = true;
if (store.appInfo === undefined && route?.params?.id !== undefined) {
const { appid, appname } = await fetchAppById(route.params.id as string);
store.setApp({
appId: appid,
name: appname
});
// only for page refreshed
const app = await fetchAppById(route.params.id as string);
store.setApp(app);
};
await store.loadFlow();
initLoading.value = false
@@ -328,10 +356,39 @@ const fetchData = async () => {
}
const fetchAppById = async(id: string) => {
const result = await api.get('api/apps');
return result.data.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
let result = await api.get('api/apps');
const app = result.data?.data?.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
if (app) {
return convertManagedAppToAppInfo(app);
}
result = await api.get(`api/v1/app?app=${id}`);
const kApp = result?.data as IAppDisplay | KErrorMsg;
if (isErrorMsg(kApp)) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: kApp.message,
});
}
return kApp;
}
type KErrorMsg = {
message: string;
}
const isErrorMsg = (e: IAppDisplay | KErrorMsg): e is KErrorMsg => {
return 'message' in e;
};
const convertManagedAppToAppInfo = (app: IManagedApp): AppInfo => {
return {
appId: app.appid,
name: app.appname
}
};
const onClearFilter=()=>{
filter.value='';
}

View File

@@ -5,7 +5,7 @@
<q-breadcrumbs-el icon="domain" label="ドメイン管理" />
</q-breadcrumbs>
</div>
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
@@ -17,12 +17,23 @@
</q-input>
</template>
<template v-slot:header-cell-active="p">
<q-th auto-width :props="p">
<q-select class="filter-header" v-model="activeFilter" :options="activeOptions" @update:model-value="activeFilterUpdate" borderless
dense options-dense hide-bottom-space/>
</q-th>
</template>
<template v-slot:body-cell-active="p">
<q-td auto-width :props="p">
<q-badge v-if="!p.row.domainActive" color="grey">未使用</q-badge>
<q-badge v-if="p.row.id == currentDomainId" color="primary">既定</q-badge>
</q-td>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
<table-action-menu :row="p.row" :actions="actionList" />
</q-td>
</template>
@@ -38,15 +49,11 @@
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="tenantid" label="テナントID" hint="テナントIDを入力してください。" lazy-rules
:rules="[val => val && val.length > 0 || 'テナントIDを入力してください。']" />
<q-input filled v-model="name" label="環境名 *" hint="kintoneの環境名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'kintoneの環境名を入力してください。']" />
<q-input filled type="url" v-model="url" label="Kintone url" hint="KintoneのURLを入力してください" lazy-rules
:rules="[val => val && val.length > 0, isDomain || 'KintoneのURLを入力してください']" />
<q-input filled type="url" v-model.trim="url" label="Kintone url" hint="KintoneのURLを入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'KintoneのURLを入力してください']" />
<q-input filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'Kintoneのログイン名を入力してください']" autocomplete="new-password" />
@@ -60,6 +67,15 @@
</template>
</q-input>
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>ドメインの有効化</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="domainActive" />
</q-item-section>
</q-item>
<div class="q-gutter-y-md" v-if="!isCreate">
<q-separator />
@@ -86,7 +102,7 @@
</q-card-section>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn label="保存" type="submit" color="primary" />
<q-btn :loading="addEditLoading" label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
@@ -96,70 +112,141 @@
<q-dialog v-model="confirm" persistent>
<q-card>
<q-card-section class="row items-center">
<q-card-section v-if="deleteLoadingState == -1" class="row items-center">
<q-spinner color="primary" size="2em"/>
<span class="q-ml-sm">ドメイン利用権限を確認中</span>
</q-card-section>
<q-card-section v-else-if="deleteLoadingState == 0" class="row items-center">
<q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</q-card-section>
<q-card-section v-else class="row items-center">
<q-icon name="error" color="negative" size="2em" />
<span class="q-ml-sm">ドメイン利用権限が存在しキャンセルする必要がある</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click="deleteDomain()" />
<q-btn v-if="deleteLoadingState > 0" label="処理に行く" color="primary" v-close-popup @click="openShareDg(editId)" />
<q-btn flat v-else label="OK" :disabled="deleteLoadingState" color="primary" v-close-popup @click="deleteDomain()" />
</q-card-actions>
</q-card>
</q-dialog>
<share-domain-dialog v-model="shareDg" :domain="shareDomain" @close="closeShareDg()" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { useDomainStore } from 'stores/useDomainStore';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import { IDomain, IDomainDisplay, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes';
const authStore = useAuthStore();
const domainStore = useDomainStore();
const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{
name: 'tenantid',
required: true,
label: 'テナントID',
field: row => row.tenantid,
format: val => `${val}`,
align: 'left',
sortable: true
},
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', },
{ name: 'actions', label: '操作', field: 'actions' }
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass },
// {
// name: 'tenantid',
// required: true,
// label: 'テナントID',
// field: 'tenantid',
// align: 'left',
// sortable: true,
// classes: inactiveRowClass
// },
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'active', label: 'x', align: 'left', field: 'domainActive', classes: inactiveRowClass },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', classes: inactiveRowClass },
{ name: 'owner', label: '所有者', field: (row: IDomainOwnerDisplay) => row.owner.fullName, align: 'left', classes: inactiveRowClass },
{ name: 'actions', label: '', field: 'actions', classes: inactiveRowClass }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const addEditLoading = ref(false);
const deleteLoadingState = ref<number>(-1); // -1: loading, 0: allow, > 0: user count
const filter = ref('');
const rows = ref([]);
const rows = ref<IDomainOwnerDisplay[]>([]);
const show = ref(false);
const confirm = ref(false);
const resetPsw = ref(false);
const tenantid = ref(authStore.currentDomain.id);
const currentDomainId = computed(() => authStore.currentDomain.id);
// const tenantid = ref(authStore.currentDomain.id);
const name = ref('');
const url = ref('');
const isPwd = ref(true);
const kintoneuser = ref('');
const kintonepwd = ref('');
const kintonepwdBK = ref('');
const domainActive = ref(true);
const isCreate = ref(true);
let editId = ref(0);
const shareDg = ref(false);
const shareDomain = ref<IDomainOwnerDisplay>({} as IDomainOwnerDisplay);
const getDomain = async () => {
const activeOptions = [
{ value: 0, label: '全状態' },
{ value: 1, label: '使用' },
{ value: 2, label: '未使用'}
]
const activeFilter = ref(activeOptions[0]);
const activeFilterUpdate = (option: {value: number}) => {
switch (option.value) {
case 1:
getDomain((row) => row.domainActive)
break;
case 2:
getDomain((row) => !row.domainActive)
break;
default:
getDomain()
break;
}
}
const actionList = [
{ label: '編集', icon: 'edit_note', action: editRow },
{ label: '利用権限設定', icon: 'person_add_alt', action: openShareDg },
{ separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
];
const getDomain = async (filter?: (row: IDomainOwnerDisplay) => boolean) => {
loading.value = true;
const userId = authStore.userId;
const result = await api.get(`api/domain?userId=${userId}`);
rows.value = result.data.map((item) => {
return { id: item.id, tenantid: item.tenantid, name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }
});
const { data } = await api.get<{data:IDomain[]}>(`api/domains`);
rows.value = data.data.map((item) => {
return {
id: item.id,
tenantid: item.tenantid,
domainActive: item.is_active,
name: item.name,
url: item.url,
user: item.kintoneuser,
password: item.kintonepwd,
owner: {
id: item.owner.id,
firstName: item.owner.first_name,
lastName: item.owner.last_name,
fullNameSearch: (item.owner.last_name + item.owner.first_name).toLowerCase(),
fullName: item.owner.last_name + ' ' + item.owner.first_name,
email: item.owner.email,
isActive: item.owner.is_active,
isSuperuser: item.owner.is_superuser,
}
}
}).filter(filter || (() => true));
loading.value = false;
}
@@ -174,27 +261,36 @@ const addRow = () => {
show.value = true;
}
const removeRow = (row) => {
async function removeRow(row: IDomainOwnerDisplay) {
confirm.value = true;
deleteLoadingState.value = -1;
editId.value = row.id;
const { data } = await api.get(`/api/domainshareduser/${row.id}`);
deleteLoadingState.value = data.data.length;
}
const deleteDomain = () => {
api.delete(`api/domain/${editId.value}`).then(() => {
api.delete(`api/domain/${editId.value}`).then(({ data }) => {
if (!data.data) {
// TODO dialog
}
getDomain();
// authStore.setCurrentDomain();
})
editId.value = 0;
editId.value = 0; // set in removeRow()
deleteLoadingState.value = -1;
};
const editRow = (row) => {
function editRow(row) {
isCreate.value = false
editId.value = row.id;
tenantid.value = row.tenantid;
// tenantid.value = row.tenantid;
name.value = row.name;
url.value = row.url;
kintoneuser.value = row.user;
kintonepwd.value = row.password;
domainActive.value = row.domainActive;
isPwd.value = true;
show.value = true;
};
@@ -214,35 +310,42 @@ const closeDg = () => {
}
const onSubmit = () => {
if (editId.value !== 0) {
api.put(`api/domain`, {
'id': editId.value,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': isCreate.value || resetPsw.value ? kintonepwd.value : ''
}).then(() => {
addEditLoading.value = true;
const method = editId.value !== 0 ? 'put' : 'post';
const param: IDomainSubmit = {
'id': editId.value,
'tenantid': '1', // TODO: テナントIDを取得する
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': ((isCreate.value && editId.value == 0) || resetPsw.value) ? kintonepwd.value : '',
'is_active': domainActive.value,
'ownerid': authStore.userId || ''
}
// for search: api.put(`api/domain`)、api.post(`api/domain`)
api[method].apply(api, [`api/domain`, param]).then(async (resp: any) => {
const res = resp.data;
if (res.data.id === currentDomainId.value && !res.data.is_active) {
await authStore.setCurrentDomain();
}
getDomain();
domainStore.loadUserDomains();
closeDg();
onReset();
addEditLoading.value = false;
})
}
else {
api.post(`api/domain`, {
'id': 0,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
}).then(() => {
getDomain();
closeDg();
onReset();
})
}
}
function openShareDg(row: IDomainOwnerDisplay|number) {
if (typeof row === 'number') {
row = rows.value.find(item => item.id === row) as IDomainOwnerDisplay;
}
shareDomain.value = row ;
shareDg.value = true;
};
function closeShareDg() {
shareDg.value = false;
}
const onReset = () => {
@@ -253,6 +356,24 @@ const onReset = () => {
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
domainActive.value = true;
resetPsw.value = false
addEditLoading.value = false;
}
</script>
<style lang="scss">
.filter-header .q-field__native {
font-size: 12px;
font-weight: 500;
}
.filter-header .q-icon {
width: 12px;
}
.q-table td.inactive-row {
color: #aaa;
background-color: #fafafa;
}
.q-table tr > td.inactive-row:last-child {
color: inherit;
}
</style>

View File

@@ -1,28 +1,19 @@
<template>
<div class="q-pa-lg">
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="assignment_ind" label="ドメイン適用" />
</q-breadcrumbs>
</div>
<q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" row-key="name"
<q-table :loading="initLoading" grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" row-key="name"
:filter="userDomainTableFilter" virtual-scroll v-model:pagination="pagination">
<template v-slot:top>
<q-btn class="q-mx-none" color="primary" label="追加" @click="clickAddDomain()" />
<q-space />
<div class="row q-gutter-md">
<q-item v-if="authStore.permissions === 'admin'" tag="label" dense @click="clickSwitchUser()">
<q-item-section>
<q-item-label>適用するユーザ : </q-item-label>
</q-item-section>
<q-item-section avatar>
{{ currentUserName }}
</q-item-section>
</q-item>
<q-input borderless dense filled debounce="300" v-model="userDomainTableFilter" placeholder="Search">
<template v-slot:append>
<q-icon name="search" />
@@ -38,67 +29,23 @@
<template v-slot:item="props">
<div class="q-pa-sm">
<q-card>
<q-card-section>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Domain</div>
<div class="q-table__grid-item-value">{{ props.row.name }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">URL</div>
<div class="q-table__grid-item-value" style="width: 22rem;">{{ props.row.url }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Account</div>
<div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div>
</div>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<div style="width: 98%;">
<div class="row items-center justify-between">
<div class="q-table__grid-item-value"
:class="isActive(props.row.id) ? 'text-positive' : 'text-negative'">{{
isActive(props.row.id)?'既定':'' }}</div>
<div class="col-auto">
<q-btn v-if="!isActive(props.row.id)" flat
@click="activeDomain(props.row.id)">既定にする</q-btn>
<q-btn flat @click="clickDeleteConfirm(props.row)">削除</q-btn>
</div>
</div>
</div>
</q-card-actions>
</q-card>
<domain-card :item="props.row" :active-id="activeDomainId">
<template v-slot:actions>
<q-card-actions align="right">
<q-chip class="no-border" v-if="isActive(props.row.id)" outline color="primary" text-color="white" icon="done">
既定
</q-chip>
<q-btn flat v-else :loading="activeDomainLoadingId === props.row.id" :disable="deleteDomainLoadingId === props.row.id" @click="activeDomain(props.row)">既定にする</q-btn>
<q-btn flat :disable="isNotOwner(props.row.owner.id) || activeDomainLoadingId === props.row.id" :text-color="isNotOwner(props.row.owner.id)?'grey':''" :loading="deleteDomainLoadingId === props.row.id" @click="clickDeleteConfirm(props.row)">削除</q-btn>
</q-card-actions>
</template>
</domain-card>
</div>
</template>
</q-table>
<show-dialog v-model:visible="showAddDomainDg" name="ドメイン" @close="addUserDomainFinished">
<domain-select ref="addDomainRef" name="ドメイン" type="multiple"></domain-select>
</show-dialog>
<show-dialog v-model:visible="showSwitchUserDd" name="ドメイン" minWidth="35vw" @close="switchUserFinished">
<template v-slot:toolbar>
<q-input dense placeholder="検索" v-model="switchUserFilter">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<div class="q-gutter-md">
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>他のユーザーを選択する</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="useOtherUser" />
</q-item-section>
</q-item>
<div v-if="useOtherUser">
<user-list ref="switchUserRef" name="ドメイン" :filter="switchUserFilter" />
</div>
</div>
<show-dialog v-model:visible="showAddDomainDg" name="ドメイン" @close="addUserDomainFinished" :ok-btn-loading="addUserDomainLoading" :ok-btn-auto-close="false">
<domain-select ref="addDomainRef" name="ドメイン" type="single" :filterInitRowsFunc="filterAddDgInitRows"></domain-select>
</show-dialog>
<q-dialog v-model="showDeleteConfirm" persistent>
@@ -123,49 +70,61 @@
import { ref, onMounted, computed } from 'vue'
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { IDomainOwnerDisplay } from '../types/DomainTypes';
import ShowDialog from 'components/ShowDialog.vue';
import DomainCard from 'components/UserDomain/DomainCard.vue';
import DomainSelect from 'components/DomainSelect.vue';
import UserList from 'components/UserList.vue';
const authStore = useAuthStore();
const pagination = ref({ sortBy: 'id', rowsPerPage: 0 });
const rows = ref([] as any[]);
const rows = ref<IDomainOwnerDisplay[]>([]);
const rowIds = new Set<string>();
const initLoading = ref(true);
const addUserDomainLoading = ref(false);
const activeDomainLoadingId = ref<number|undefined>(undefined);
const deleteDomainLoadingId = ref<number|undefined>(undefined);
const columns = [
{ name: 'id' },
{ name: 'name', required: true, label: 'Name', align: 'left', field: 'name', sortable: true },
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' },
{ name: 'active', field: 'active' }
{ name: 'kintoneuser', label: 'User', field: 'user', sortable: true },
];
const userDomainTableFilter = ref();
const currentUserName = ref('');
const useOtherUser = ref(false);
const otherUserId = ref('');
let editId = ref(0);
const showAddDomainDg = ref(false);
const addDomainRef = ref();
const filterAddDgInitRows = (row: {domainActive: boolean, id: string}) => {
return row.domainActive && !rowIds.has(row.id);
}
const clickAddDomain = () => {
editId.value = 0;
showAddDomainDg.value = true;
};
const addUserDomainFinished = (val: string) => {
if (val == 'OK') {
let dodmainids = [];
let domains = JSON.parse(JSON.stringify(addDomainRef.value.selected));
for (var key in domains) {
dodmainids.push(domains[key].id);
const addUserDomainFinished = async (val: string) => {
showAddDomainDg.value = true;
const selected = addDomainRef.value.selected;
if (val == 'OK' && selected.length > 0) {
addUserDomainLoading.value = true;
const { data } = await api.post(`api/domain/${authStore.userId}?domainid=${selected[0].id}`)
if (rows.value.length === 0 && data.data) {
const domain = data.data;
await authStore.setCurrentDomain({
id: domain.id,
kintoneUrl: domain.url,
domainName: domain.name
});
}
api.post(`api/domain/${useOtherUser.value ? otherUserId.value : authStore.userId}`, dodmainids)
.then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); });
await getDomain();
}
addUserDomainLoading.value = false;
showAddDomainDg.value = false;
};
const showDeleteConfirm = ref(false);
@@ -175,16 +134,26 @@ const clickDeleteConfirm = (row: any) => {
editId.value = row.id;
};
const deleteDomainFinished = () => {
api.delete(`api/domain/${editId.value}/${useOtherUser.value ? otherUserId.value : authStore.userId}`).then(() => {
getDomain(useOtherUser.value ? otherUserId.value : undefined);
})
const deleteDomainFinished = async () => {
deleteDomainLoadingId.value = editId.value;
const { data } = await api.delete(`api/domain/${editId.value}/${authStore.userId}`)
if (data.msg == 'OK' && authStore.currentDomain.id === editId.value) {
authStore.setCurrentDomain();
}
editId.value = 0;
await getDomain();
deleteDomainLoadingId.value = undefined;
};
const activeDomain = (id: number) => {
api.put(`api/activedomain/${id}${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`)
.then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); })
const activeDomain = async (domain: any) => {
activeDomainLoadingId.value = domain.id;
await authStore.setCurrentDomain({
id: domain.id,
kintoneUrl: domain.url,
domainName: domain.name
});
await getDomain();
activeDomainLoadingId.value = undefined;
};
let activeDomainId = ref(0);
@@ -193,44 +162,58 @@ const isActive = computed(() => (id: number) => {
return id == activeDomainId.value;
});
const showSwitchUserDd = ref(false);
const switchUserRef = ref();
const switchUserFilter = ref('')
const clickSwitchUser = () => {
showSwitchUserDd.value = true;
useOtherUser.value = false;
};
const switchUserFinished = async (val: string) => {
if (val == 'OK') {
if (useOtherUser.value) {
const user = switchUserRef.value.selected[0]
currentUserName.value = user.email;
otherUserId.value = user.id
await getDomain(user.id)
} else {
currentUserName.value = authStore.userInfo.email
await getDomain();
}
}
};
const isNotOwner = computed(() => (ownerId: string) => {
return ownerId !== authStore.userId;
});
const getDomain = async (userId? : string) => {
const resp = await api.get(`api/activedomain${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`);
activeDomainId.value = resp?.data?.id;
rowIds.clear();
const resp = await api.get(`api/defaultdomain`);
activeDomainId.value = resp?.data?.data?.id;
const domainResult = userId ? await api.get(`api/domain?userId=${userId}`) : await api.get(`api/domain`);
const domains = domainResult.data as any[];
rows.value = domains.map((item) => {
return { id: item.id, name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd }
});
rows.value = domains.sort((a, b) => a.id - b.id).reduce((acc, item) => {
rowIds.add(item.id);
if (item.is_active) {
acc.push({
id: item.id,
tenantid: item.tenantid,
domainActive: item.is_active,
name: item.name,
url: item.url,
user: item.kintoneuser,
password: item.kintonepwd,
owner: {
id: item.owner.id,
firstName: item.owner.first_name,
lastName: item.owner.last_name,
fullNameSearch: (item.owner.last_name + item.owner.first_name).toLowerCase(),
fullName: item.owner.last_name + ' ' + item.owner.first_name,
email: item.owner.email,
isActive: item.owner.is_active,
isSuperuser: item.owner.is_superuser,
}
})
}
return acc;
}, []);
}
onMounted(async () => {
currentUserName.value = authStore.userInfo.email
initLoading.value = true;
await getDomain();
initLoading.value = false;
})
</script>
<style lang="scss" scoped>
.domain-card {
width: 22rem;
word-break: break-word;
.smaller-font-size {
font-size: 13px;
}
}
</style>

View File

@@ -65,10 +65,10 @@
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="firstName" label="氏名 *" hint="ユーザーの氏名を入力してください" lazy-rules
<q-input filled v-model="lastName" label="氏名 *" hint="ユーザーの氏名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'ユーザーの氏名を入力してください。']" />
<q-input filled v-model="lastName" label="苗字 *" hint="ユーザーの苗字を入力してください" lazy-rules
<q-input filled v-model="firstName" label="苗字 *" hint="ユーザーの苗字を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'ユーザーの苗字を入力してください']" />
<q-input filled type="email" v-model="email" label="電子メール *" hint="電子メール、ログインとしても使用" lazy-rules
@@ -127,7 +127,7 @@
</q-card-section>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn label="保存" type="submit" color="primary" />
<q-btn :loading="addEditLoading" label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
@@ -158,8 +158,8 @@ import { api } from 'boot/axios';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'firstName', label: '氏名', field: 'firstName', align: 'left', sortable: true },
{ name: 'lastName', label: '苗字', field: 'lastName', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'actions', label: '操作', field: 'actions' }
@@ -168,6 +168,7 @@ const columns = [
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const addEditLoading = ref(false);
const filter = ref('');
const statusFilter = ref('全データ');
const rows = ref([]);
@@ -189,7 +190,7 @@ let editId = ref(0);
const getUsers = async (filter = () => true) => {
loading.value = true;
const result = await api.get(`api/v1/users`);
rows.value = result.data.map((item) => {
rows.value = result.data.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
}).filter(filter);
loading.value = false;
@@ -260,6 +261,7 @@ const closeDg = () => {
}
const onSubmit = () => {
addEditLoading.value = true;
if (editId.value !== 0) {
api.put(`api/v1/users/${editId.value}`, {
'first_name': firstName.value,
@@ -302,6 +304,7 @@ const onReset = () => {
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
resetPsw.value = false
resetPsw.value = false;
addEditLoading.value = false;
}
</script>

View File

@@ -5,6 +5,7 @@ import {
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { Dialog } from 'quasar'
import routes from './routes';
import { useAuthStore } from 'stores/useAuthStore';
@@ -47,6 +48,18 @@ export default route(function (/* { store, ssrContext } */) {
authStore.returnUrl = to.fullPath;
return '/login';
}
// redirect to domain setting page if no domain exist
const domainPages = [...publicPages, '/domain', '/userDomain', '/user'];
if (!authStore.hasDomain && !domainPages.includes(to.path)) {
Dialog.create({
title: '注意',
message: '既定/利用可能なドメインはありません。<br>ドメイン管理ページに遷移して処理します。',
html: true,
persistent: true,
})
return '/domain';
}
});
return routerInstance;
});

View File

@@ -25,7 +25,7 @@ const routes: RouteRecordRaw[] = [
// { path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.vue') },
// { path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'user', component: () => import('pages/UserManagement.vue')},
{ path: 'app', component: () => import('pages/AppManagement.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') }

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { router } from 'src/router';
import { IDomainInfo } from '../types/ActionTypes';
import { IDomainInfo } from '../types/DomainTypes';
import { jwtDecode } from 'jwt-decode';
interface UserInfo {
firstName: string;
@@ -16,7 +16,8 @@ export interface IUserState {
LeftDrawer: boolean;
userId?: string;
userInfo: UserInfo;
permissions: 'admin' | 'user';
roles:string,
permissions: string;
}
export const useAuthStore = defineStore('auth', {
@@ -27,12 +28,16 @@ export const useAuthStore = defineStore('auth', {
currentDomain: {} as IDomainInfo,
userId: '',
userInfo: {} as UserInfo,
permissions: 'user',
roles:'',
permissions: '',
}),
getters: {
toggleLeftDrawer(): boolean {
return this.LeftDrawer;
},
hasDomain(): boolean {
return this.currentDomain.id !== undefined;
}
},
actions: {
setLeftMenu(value:boolean){
@@ -51,7 +56,7 @@ export const useAuthStore = defineStore('auth', {
this.token = result.data.access_token;
const tokenJson = jwtDecode(result.data.access_token);
this.userId = tokenJson.sub;
this.permissions = (tokenJson as any).permissions ?? 'user';
this.permissions = (tokenJson as any).permissions==='ALL' ? 'admin': 'user';
api.defaults.headers['Authorization'] = 'Bearer ' + this.token;
this.currentDomain = await this.getCurrentDomain();
this.userInfo = await this.getUserInfo();
@@ -63,25 +68,16 @@ export const useAuthStore = defineStore('auth', {
}
},
async getCurrentDomain(): Promise<IDomainInfo> {
const resp = await api.get(`api/activedomain`);
const activedomain = resp?.data;
const resp = await api.get(`api/defaultdomain`);
const activedomain = resp?.data?.data;
return {
id: activedomain?.id,
domainName: activedomain?.name,
kintoneUrl: activedomain?.url,
};
},
async getUserDomains(): Promise<IDomainInfo[]> {
const resp = await api.get(`api/domain`);
const domains = resp.data as any[];
return domains.map((data) => ({
id: data.id,
domainName: data.name,
kintoneUrl: data.url,
}));
},
async getUserInfo():Promise<UserInfo>{
const resp = (await api.get(`api/v1/users/me`)).data;
const resp = (await api.get(`api/v1/users/me`)).data.data;
return {
firstName: resp.first_name,
lastName: resp.last_name,
@@ -93,11 +89,15 @@ export const useAuthStore = defineStore('auth', {
this.currentDomain = {} as IDomainInfo; // 清空当前域
router.push('/login');
},
async setCurrentDomain(domain: IDomainInfo) {
async setCurrentDomain(domain?: IDomainInfo) {
if (!domain) {
this.currentDomain = {} as IDomainInfo;
return;
}
if (domain.id === this.currentDomain.id) {
return;
}
await api.put(`api/activedomain/${domain.id}`);
await api.put(`api/defaultdomain/${domain.id}`);
this.currentDomain = domain;
},
},

View File

@@ -0,0 +1,23 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { IDomainInfo, IDomain } from '../types/DomainTypes';
export const useDomainStore = defineStore('domain', {
state: () => ({
userDomains: [] as IDomainInfo[],
}),
actions: {
async loadUserDomains(): Promise<IDomainInfo[]> {
const resp = await api.get(`api/domain`);
const domains = resp.data as IDomain[];
this.userDomains = domains
.filter(data => data.is_active)
.map((data) => ({
id: data.id,
domainName: data.name,
kintoneUrl: data.url,
}));
return this.userDomains;
},
},
});

View File

@@ -1,9 +1,4 @@
import { v4 as uuidv4 } from 'uuid';
export interface IDomainInfo{
id:number;
domainName:string;
kintoneUrl:string;
}
/**
* アプリ情報

View File

@@ -1,14 +1,51 @@
interface IUser {
first_name: string;
last_name: string;
email: string;
}
export interface IManagedApp {
appid: string;
appname: string;
domainurl: string;
version: string;
updateuser: IUser;
update_time: string;
}
import { IUser, IUserDisplay } from './UserTypes';
export interface IManagedApp {
appid: string;
appname: string;
domainurl: string;
version: string;
user: IUser;
updateuser: IUser;
create_time: string;
update_time: string;
}
export interface IManagedApp {
appid: string;
appname: string;
domainurl: string;
version: string;
user: IUser;
updateuser: IUser;
create_time: string;
update_time: string;
}
export interface IAppDisplay{
id:string;
sortId: number;
name:string;
url:string;
updateUser: IUserDisplay;
updateTime:string;
version:string;
}
export interface IVersionInfo {
id: string;
name?: string;
desc?: string;
}
export interface IAppVersion {
id: number;
version: number;
appid: string;
name: string
comment: string;
updater: IUserDisplay;
updateTime: string;
creator: IUserDisplay;
createTime: string;
}

View File

@@ -0,0 +1,38 @@
import { IUser, IUserDisplay } from './UserTypes';
export interface IDomainInfo {
id: number;
domainName: string;
kintoneUrl: string;
}
export interface IDomain {
id: number;
tenantid: string;
name: string;
url: string;
kintoneuser: string,
kintonepwd: string,
create_time: string;
update_time: string;
is_active: boolean;
owner: IUser
}
export interface IDomainSubmit extends Omit<IDomain, 'create_time' | 'update_time' | 'owner'> {
ownerid: string;
}
export interface IDomainDisplay {
id: number;
tenantid: string;
name: string;
url: string;
user: string;
password?: string;
domainActive: boolean;
}
export interface IDomainOwnerDisplay extends IDomainDisplay {
owner: IUserDisplay
}

View File

@@ -0,0 +1,24 @@
export interface IUser {
id: number;
first_name: string;
last_name: string;
email: string;
is_active: boolean,
is_superuser: boolean,
roles: object[]
}
export interface IUserDisplay {
id: number;
firstName: string;
lastName: string;
fullName: string;
fullNameSearch: string;
email: string;
isActive: boolean,
isSuperuser: boolean,
}
export interface IUserRolesDisplay extends IUserDisplay {
roles: object[]
}