├── .anvil_editor.yaml ├── .gitignore ├── README.md ├── __init__.py ├── anvil.yaml ├── client_code ├── Home │ ├── Header │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── RowTemplate1 │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── __init__.py │ └── form_template.yaml ├── Machines │ ├── MachineDetail │ │ ├── MachineRouteRow │ │ │ ├── __init__.py │ │ │ └── form_template.yaml │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── MachineRow │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── __init__.py │ └── form_template.yaml ├── Routes │ ├── RouteRow │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── __init__.py │ └── form_template.yaml ├── Settings │ ├── AddUser │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── AppUsers │ │ ├── AppUsersTemplate │ │ │ ├── __init__.py │ │ │ └── form_template.yaml │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── __init__.py │ └── form_template.yaml ├── Startup.py └── Users │ ├── PreAuth │ ├── GenerateKey │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── PreAuthKeyRow │ │ ├── __init__.py │ │ └── form_template.yaml │ ├── __init__.py │ └── form_template.yaml │ ├── UserRow │ ├── __init__.py │ └── form_template.yaml │ ├── __init__.py │ └── form_template.yaml ├── server_code ├── API_Interface.py ├── Hostess.py └── RecordRecorder.py └── theme ├── assets ├── milliner_logo.png ├── milliner_spinner.png ├── standard-page.html └── theme.css ├── parameters.yaml └── templates.yaml /.anvil_editor.yaml: -------------------------------------------------------------------------------- 1 | unique_ids: 2 | forms: 3 | Users.PreAuth: '1680917206140202361516214.53912' 4 | Machines: '1680787304124770418668583.75' 5 | Routes: '1680792592718148150989093.66742' 6 | Home.RowTemplate1: '1680742075787691035143944.1995' 7 | Settings.AppUsers: '1680818557115792849019454.5806' 8 | Settings.AddUser: '1680814292012935083795394.9249' 9 | Users.PreAuth.GenerateKey: '1680931175859716587359885.5071' 10 | Machines.MachineDetail: '1680876986352129103239699.90977' 11 | Settings: '1680802990379829050247630.9993' 12 | Users: '1680789066429724812072809.3873' 13 | Routes.RouteRow: '1680792604662563304920925.5035' 14 | Users.PreAuth.PreAuthKeyRow: '1680917272692747028879605.7814' 15 | Users.UserRow: '1680828924011617711773402.4417' 16 | Machines.MachineRow: '1680787330601762866562982.6748' 17 | Settings.AppUsers.AppUsersTemplate: '1680808528309286578763962.20306' 18 | Machines.MachineDetail.MachineRouteRow: '1680879268230914845955877.3297' 19 | Home: HX4XYKOBKDJLASEPJRK5E47RME4D2VT3 20 | Home.Header: '168097609810740373230911.51868' 21 | modules: 22 | Startup: '1681078834820313478862549.32135' 23 | server_modules: 24 | API_Interface: '1680742140322703776832328.0709' 25 | Hostess: '1680787671616907323215116.5988' 26 | RecordRecorder: '1680784797723764777233107.3958' 27 | assets: 28 | milliner_logo.png: '1680796733581114653423428.42722' 29 | milliner_spinner.png: '1680870959214572130263182.6436' 30 | standard-page.html: VM5NH54ERXEK273777JO4IMYNUOIM4BZ 31 | theme.css: QDIZM2OVOKIZBAXMIRPNEDTR27VECD23 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | __pycache__ 4 | .anvil-data 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About This [Anvil](https://anvil.works/?utm_source=github:app_README) App 2 | 3 | ### Build web apps with nothing but Python. 4 | 5 | The app in this repository is built with [Anvil](https://anvil.works?utm_source=github:app_README), the framework for building web apps with nothing but Python. You can clone this app into your own Anvil account to use and modify. 6 | 7 | Below, you will find: 8 | - [How to open this app](#opening-this-app-in-anvil-and-getting-it-online) in Anvil and deploy it online 9 | - Information [about Anvil](#about-anvil) 10 | - And links to some handy [documentation and tutorials](#tutorials-and-documentation) 11 | 12 | ## Opening this app in Anvil and getting it online 13 | 14 | ### Cloning the app 15 | 16 | Go to the [Anvil Editor](https://anvil.works/build?utm_source=github:app_README) (you might need to sign up for a free account) and click on “Clone from GitHub” (underneath the “Blank App” option): 17 | 18 | Clone from GitHub 19 | 20 | Enter the URL of this GitHub repository. If you're not yet logged in, choose "GitHub credentials" as the authentication method and click "Connect to GitHub". 21 | 22 | Clone App from Git modal 23 | 24 | Finally, click "Clone App". 25 | 26 | This app will then be in your Anvil account, ready for you to run it or start editing it! **Any changes you make will be automatically pushed back to this repository, if you have permission!** You might want to [make a new branch](https://anvil.works/docs/version-control-new-ide?utm_source=github:app_README). 27 | 28 | ### Running the app yourself: 29 | 30 | Find the **Run** button at the top-right of the Anvil editor: 31 | 32 | 33 | 34 | 35 | ### Publishing the app on your own URL 36 | 37 | Now you've cloned the app, you can [deploy it on the internet with two clicks](https://anvil.works/docs/deployment/quickstart?utm_source=github:app_README)! Find the **Publish** button at the top-right of the editor: 38 | 39 | 40 | 41 | When you click it, you will see the Publish dialog: 42 | 43 | 44 | 45 | Click **Publish This App**, and you will see that your app has been deployed at a new, public URL: 46 | 47 | 48 | 49 | That's it - **your app is now online**. Click the link and try it! 50 | 51 | ## About Anvil 52 | 53 | If you’re new to Anvil, welcome! Anvil is a platform for building full-stack web apps with nothing but Python. No need to wrestle with JS, HTML, CSS, Python, SQL and all their frameworks – just build it all in Python. 54 | 55 |
56 |

Learn About Anvil In 80 Seconds👇

57 | 58 | Anvil In 80 Seconds 62 | 63 |
64 |

65 | 66 | [![Try Anvil Free](https://anvil-website-static.s3.eu-west-2.amazonaws.com/mark-complete.png)](https://anvil.works?utm_source=github:app_README) 67 | 68 | To learn more about Anvil, visit [https://anvil.works](https://anvil.works?utm_source=github:app_README). 69 | 70 | ## Tutorials and documentation 71 | 72 | ### Tutorials 73 | 74 | If you are just starting out with Anvil, why not **[try the 10-minute Feedback Form tutorial](https://anvil.works/learn/tutorials/feedback-form?utm_source=github:app_README)**? It features step-by-step tutorials that will introduce you to the most important parts of Anvil. 75 | 76 | Anvil has tutorials on: 77 | - [Building Dashboards](https://anvil.works/learn/tutorials/data-science#dashboarding?utm_source=github:app_README) 78 | - [Multi-User Applications](https://anvil.works/learn/tutorials/multi-user-apps?utm_source=github:app_README) 79 | - [Building Web Apps with an External Database](https://anvil.works/learn/tutorials/external-database?utm_source=github:app_README) 80 | - [Deploying Machine Learning Models](https://anvil.works/learn/tutorials/deploy-machine-learning-model?utm_source=github:app_README) 81 | - [Taking Payments with Stripe](https://anvil.works/learn/tutorials/stripe?utm_source=github:app_README) 82 | - And [much more....](https://anvil.works/learn/tutorials?utm_source=github:app_README) 83 | 84 | ### Reference Documentation 85 | 86 | The Anvil reference documentation provides comprehensive information on how to use Anvil to build web applications. You can find the documentation [here](https://anvil.works/docs/overview?utm_source=github:app_README). 87 | 88 | If you want to get to the basics as quickly as possible, each section of this documentation features a [Quick-Start Guide](https://anvil.works/docs/overview/quickstarts?utm_source=github:app_README). 89 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # This repository is an Anvil app. Learn more at https://anvil.works/ 3 | # To run the server-side code on your own machine, run: 4 | # pip install anvil-uplink 5 | # python -m anvil.run_app_via_uplink YourAppPackageName 6 | 7 | __path__ = [__path__[0] + "/server_code", __path__[0] + "/client_code"] 8 | -------------------------------------------------------------------------------- /anvil.yaml: -------------------------------------------------------------------------------- 1 | scheduled_tasks: 2 | - task_name: record_machines 3 | time_spec: 4 | n: 5 5 | every: minute 6 | at: {} 7 | job_id: JMAKXPDN 8 | - task_name: record_users 9 | time_spec: 10 | n: 5 11 | every: minute 12 | at: {} 13 | job_id: BJVVVHAS 14 | - task_name: record_routes 15 | time_spec: 16 | n: 5 17 | every: minute 18 | at: {} 19 | job_id: FAGDAFJR 20 | services: 21 | - source: /runtime/services/tables.yml 22 | client_config: {enable_v2: true} 23 | server_config: {auto_create_missing_columns: false} 24 | - source: /runtime/services/anvil/users.yml 25 | client_config: {allow_signup: false, enable_automatically: true, use_email: true, 26 | confirm_email: false, allow_remember_me: true, remember_me_days: 30} 27 | server_config: {user_table: users} 28 | startup: {type: module, module: Startup} 29 | package_name: Milliner 30 | allow_embedding: false 31 | name: Milliner 32 | runtime_options: 33 | version: 2 34 | client_version: '3' 35 | server_version: python3-full 36 | server_spec: null 37 | server_spec_disabled: {base: python310-standard, requirements: protobuf} 38 | metadata: {title: Milliner, description: A simple UI to control a Headscale implementation., 39 | logo_img: 'asset:milliner_logo.png'} 40 | startup_form: null 41 | native_deps: {head_html: ''} 43 | db_schema: 44 | machines: 45 | title: Machines 46 | client: none 47 | server: full 48 | columns: 49 | - name: name 50 | admin_ui: {} 51 | type: string 52 | - name: ipAddr 53 | admin_ui: {} 54 | type: simpleObject 55 | - name: discoKey 56 | admin_ui: {} 57 | type: string 58 | - name: expiry 59 | admin_ui: {} 60 | type: string 61 | - name: registerMethod 62 | admin_ui: {} 63 | type: string 64 | - name: machineKey 65 | admin_ui: {} 66 | type: string 67 | - name: createdAt 68 | admin_ui: {} 69 | type: string 70 | - name: lastSuccessfulUpdate 71 | admin_ui: {} 72 | type: string 73 | - name: nodeKey 74 | admin_ui: {} 75 | type: string 76 | - name: lastSeen 77 | admin_ui: {} 78 | type: string 79 | - name: user 80 | admin_ui: {} 81 | type: simpleObject 82 | - name: id 83 | admin_ui: {} 84 | type: string 85 | - name: givenName 86 | admin_ui: {} 87 | type: string 88 | - name: online 89 | admin_ui: {} 90 | type: bool 91 | - name: invalidTags 92 | admin_ui: {order: 17, width: 200} 93 | type: simpleObject 94 | - name: validTags 95 | admin_ui: {order: 17, width: 200} 96 | type: simpleObject 97 | - name: forcedTags 98 | admin_ui: {order: 17, width: 200} 99 | type: simpleObject 100 | - name: preAuthKey 101 | admin_ui: {order: 17, width: 200} 102 | type: simpleObject 103 | users: 104 | title: Users 105 | client: none 106 | server: full 107 | columns: 108 | - name: email 109 | admin_ui: {width: 200} 110 | type: string 111 | - name: enabled 112 | admin_ui: {width: 200} 113 | type: bool 114 | - name: last_login 115 | admin_ui: {width: 200} 116 | type: datetime 117 | - name: password_hash 118 | admin_ui: {width: 200} 119 | type: string 120 | - name: n_password_failures 121 | admin_ui: {width: 141} 122 | type: number 123 | - name: confirmed_email 124 | admin_ui: {width: 200} 125 | type: bool 126 | - name: signed_up 127 | admin_ui: {width: 200} 128 | type: datetime 129 | - name: remembered_logins 130 | admin_ui: {width: 200} 131 | type: simpleObject 132 | settings: 133 | title: Settings 134 | client: full 135 | server: full 136 | columns: 137 | - name: url 138 | admin_ui: {width: 200} 139 | type: string 140 | - name: api_key 141 | admin_ui: {width: 200} 142 | type: string 143 | - name: last_hs_sync 144 | admin_ui: {width: 200} 145 | type: string 146 | - name: api_key_expiration 147 | admin_ui: {width: 200} 148 | type: string 149 | - name: api_key_creation 150 | admin_ui: {width: 200} 151 | type: string 152 | - name: is_fresh_install 153 | admin_ui: {order: 5, width: 200} 154 | type: bool 155 | hs_users: 156 | title: hs_users 157 | client: none 158 | server: full 159 | columns: 160 | - name: id 161 | admin_ui: {width: 200} 162 | type: string 163 | - name: name 164 | admin_ui: {} 165 | type: string 166 | - name: createdAt 167 | admin_ui: {} 168 | type: string 169 | routes: 170 | title: Routes 171 | client: none 172 | server: full 173 | columns: 174 | - {name: givenName, admin_ui: null, type: string} 175 | - name: id 176 | admin_ui: {width: 200} 177 | type: number 178 | - name: prefix 179 | admin_ui: {} 180 | type: string 181 | - name: machineName 182 | admin_ui: {} 183 | type: string 184 | - name: machineIPs 185 | admin_ui: {} 186 | type: simpleObject 187 | - name: enabled 188 | admin_ui: {} 189 | type: bool 190 | -------------------------------------------------------------------------------- /client_code/Home/Header/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import HeaderTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from ... import Startup 9 | 10 | class Header(HeaderTemplate): 11 | def __init__(self, **properties): 12 | # Set Form properties and Data Bindings. 13 | self.user = Startup.user 14 | if self.user: 15 | self.link_user.text = self.user['email'] 16 | else: 17 | self.link_user.text = 'None' 18 | self.init_components(**properties) 19 | 20 | # Any code you write here will run before the form opens. 21 | 22 | def link_user_click(self, **event_args): 23 | """This method is called when the link is clicked""" 24 | anvil.users.configure_account_with_form() 25 | 26 | def button_sign_out_click(self, **event_args): 27 | """This method is called when the button is clicked""" 28 | get_open_form().column_panel_home.clear() 29 | anvil.users.logout() 30 | anvil.users.login_with_form() 31 | open_form('Home') 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /client_code/Home/Header/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: GridPanel 6 | properties: {spacing_above: small, spacing_below: small, background: '', foreground: '', 7 | border: '', visible: true, role: null, tooltip: ''} 8 | name: grid_panel_header 9 | layout_properties: {grid_position: 'YPTHJP,ELKNLH', full_width_row: true} 10 | components: 11 | - type: ColumnPanel 12 | properties: {col_widths: '{"WEFXQI":25,"XTTDOG":35}'} 13 | name: column_panel_1 14 | layout_properties: {row: VTIJMM, width_xs: 5, col_xs: 7} 15 | components: 16 | - type: Link 17 | properties: {role: null, url: '', align: right, tooltip: '', border: '', foreground: '', 18 | visible: true, text: '', font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 19 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small, 20 | italic: false, background: '', bold: false, underline: false, icon: 'fa:user'} 21 | name: link_user 22 | layout_properties: {row: VTIJMM, width_xs: 3, col_xs: 8, width: 136.65625, grid_position: 'ZJLUUA,XTTDOG'} 23 | data_bindings: [] 24 | event_bindings: {click: link_user_click} 25 | components: [] 26 | - type: Button 27 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true, 28 | foreground: '', visible: true, text: Sign Out, font_size: null, font: '', 29 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 30 | background: '', bold: false, underline: false, icon: 'fa:sign-out'} 31 | name: button_sign_out 32 | layout_properties: {row: VTIJMM, width_xs: 3, col_xs: 8, width: 159.328125, 33 | grid_position: 'ZJLUUA,WEFXQI'} 34 | event_bindings: {click: button_sign_out_click} 35 | is_package: true 36 | custom_component: true 37 | -------------------------------------------------------------------------------- /client_code/Home/RowTemplate1/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import RowTemplate1Template 2 | from anvil import * 3 | import anvil.users 4 | import anvil.server 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class RowTemplate1(RowTemplate1Template): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings. 12 | self.init_components(**properties) 13 | 14 | # Any code you write here will run before the form opens. 15 | -------------------------------------------------------------------------------- /client_code/Home/RowTemplate1/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: {type: DataRowPanel} 2 | components: [] 3 | is_package: true 4 | -------------------------------------------------------------------------------- /client_code/Home/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import HomeTemplate 2 | from anvil import * 3 | import anvil.users 4 | import anvil.server 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from ..Machines import Machines 9 | from ..Users import Users 10 | from ..Routes import Routes 11 | from ..Settings import Settings 12 | from .. import Startup 13 | 14 | class Home(HomeTemplate): 15 | def __init__(self, **properties): 16 | if app_tables.settings.get(): 17 | self.label_sync_time.text = [r['last_hs_sync'] for r in app_tables.settings.search()][0] 18 | else: 19 | self.label_sync_time.text = 'Never Synced' 20 | self.item = anvil.server.call('get_machine_table').search(online=True) 21 | self.version = Startup.version 22 | self.url = Startup.url 23 | self.api_key = Startup.api_key 24 | # Set Form properties and Data Bindings. 25 | self.init_components(**properties) 26 | 27 | 28 | # Any code you write here will run before the form opens. 29 | 30 | def form_show(self, **event_args): 31 | """This method is called when the HTML panel is shown on the screen""" 32 | if self.url == "": 33 | alert('No URL has been set. The application will not function until one is set in Settings.', title='No URL!') 34 | if self.api_key == "": 35 | alert('No API Key has been set. The application will not function until one is set in Settings.', title='No API Key!') 36 | 37 | def link_machines_click(self, **event_args): 38 | """This method is called when the link is clicked""" 39 | self.column_panel_home.clear() 40 | self.column_panel_home.add_component(Machines(), full_width_row=True) 41 | 42 | def link_users_click(self, **event_args): 43 | """This method is called when the link is clicked""" 44 | self.column_panel_home.clear() 45 | self.column_panel_home.add_component(Users()) 46 | 47 | def link_user_click(self, **event_args): 48 | """This method is called when the link is clicked""" 49 | anvil.users.configure_account_with_form() 50 | 51 | def image_logo_mouse_down(self, x, y, button, **event_args): 52 | """This method is called when a mouse button is pressed on this component""" 53 | open_form('Home') 54 | 55 | def link_home_click(self, **event_args): 56 | """This method is called when the link is clicked""" 57 | open_form('Home') 58 | 59 | def link_routes_click(self, **event_args): 60 | """This method is called when the link is clicked""" 61 | self.column_panel_home.clear() 62 | self.column_panel_home.add_component(Routes()) 63 | 64 | def link_settings_click(self, **event_args): 65 | """This method is called when the link is clicked""" 66 | self.column_panel_home.clear() 67 | self.column_panel_home.add_component(Settings()) 68 | 69 | def button_refresh_data_click(self, **event_args): 70 | """This method is called when the button is clicked""" 71 | with Notification('Refreshing Data from Headscale'): 72 | anvil.server.call('record_users') 73 | anvil.server.call('record_machines') 74 | anvil.server.call('record_routes') 75 | self.item = anvil.server.call('get_machine_table').search(online=True) 76 | self.label_sync_time.text = [r['last_hs_sync'] for r in app_tables.settings.search()][0] 77 | 78 | def button_test_function_click(self, **event_args): 79 | """This method is called when the button is clicked""" 80 | alert(anvil.server.call('get_api_key_info', self.url, self.api_key)) 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /client_code/Home/form_template.yaml: -------------------------------------------------------------------------------- 1 | is_package: true 2 | container: 3 | type: HtmlTemplate 4 | properties: {html: '@theme:standard-page.html'} 5 | event_bindings: {show: form_show} 6 | components: 7 | - type: ColumnPanel 8 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true, 9 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{}', 10 | spacing_below: small, background: ''} 11 | name: column_panel_home 12 | layout_properties: {slot: default} 13 | components: 14 | - type: form:Home.Header 15 | properties: {} 16 | name: header_1 17 | layout_properties: {grid_position: 'TMCTOK,AUCFBT', full_width_row: false} 18 | data_bindings: [] 19 | - type: Label 20 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '', 21 | visible: true, text: Online Machines, font_size: null, font: '', spacing_above: none, 22 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false, 23 | underline: true, icon: ''} 24 | name: label_1 25 | layout_properties: {row: YDPWFW, width_xs: 2, col_xs: 5, grid_position: 'HWODQZ,PSOSUQ'} 26 | - type: DataGrid 27 | properties: 28 | role: null 29 | columns: 30 | - {id: JFGZMI, title: Id, data_key: id, $$hashKey: 'object:2411', width: '50'} 31 | - {id: EWYRCD, title: Name, data_key: givenName, $$hashKey: 'object:2397', width: '175', 32 | expand: true} 33 | - {id: CWEWNV, title: IpAddr, data_key: ipAddr, $$hashKey: 'object:2398', width: '175', 34 | expand: true} 35 | - {id: VDHOSS, title: LastSeen, data_key: lastSeen, $$hashKey: 'object:2408', 36 | width: '175', expand: true} 37 | - {id: YXSWKC, title: User, data_key: user, $$hashKey: 'object:2410', width: '125'} 38 | auto_header: true 39 | tooltip: '' 40 | border: '' 41 | foreground: '' 42 | rows_per_page: 20 43 | visible: true 44 | wrap_on: never 45 | show_page_controls: true 46 | spacing_above: small 47 | spacing_below: small 48 | background: '' 49 | name: data_grid_1 50 | layout_properties: {grid_position: 'UDSNGJ,MEHURZ'} 51 | components: 52 | - type: RepeatingPanel 53 | properties: {spacing_above: none, spacing_below: none, item_template: Home.RowTemplate1} 54 | name: repeating_panel_1 55 | layout_properties: {} 56 | data_bindings: 57 | - {property: items, code: self.item} 58 | - type: GridPanel 59 | properties: {spacing_above: none, spacing_below: none, background: '', foreground: '', 60 | border: '', visible: true, role: null, tooltip: ''} 61 | name: grid_panel_footer 62 | layout_properties: {slot: footer} 63 | components: 64 | - type: Button 65 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true, 66 | foreground: '', visible: true, text: Refresh Data, font_size: null, font: '', 67 | spacing_above: small, icon_align: left, spacing_below: none, italic: false, 68 | background: '', bold: false, underline: false, icon: 'fa:refresh'} 69 | name: button_refresh_data 70 | layout_properties: {row: WXHJII, width_xs: 3, col_xs: 0, width: 180.1875} 71 | event_bindings: {click: button_refresh_data_click} 72 | - type: ColumnPanel 73 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true, 74 | wrap_on: mobile, col_spacing: medium, spacing_above: none, col_widths: '{"HILOMR":15,"WWHTIK":45}', 75 | spacing_below: none, background: ''} 76 | name: column_panel_2 77 | layout_properties: {row: WXHJII, width_xs: 5, col_xs: 3, width: 320.3125} 78 | components: 79 | - type: Label 80 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 81 | visible: true, text: 'Last Sync:', font_size: null, font: '', spacing_above: small, 82 | icon_align: left, spacing_below: none, italic: false, background: '', 83 | bold: false, underline: false, icon: ''} 84 | name: label_sync 85 | layout_properties: {grid_position: 'RIIETN,HILOMR'} 86 | - type: Label 87 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 88 | visible: true, text: '', font_size: null, font: '', spacing_above: small, 89 | icon_align: left, spacing_below: none, italic: false, background: '', 90 | bold: false, underline: false, icon: ''} 91 | name: label_sync_time 92 | layout_properties: {grid_position: 'RIIETN,WWHTIK'} 93 | - type: Image 94 | properties: {role: null, vertical_align: center, height: 93, tooltip: '', border: '', 95 | foreground: '', visible: true, display_mode: shrink_to_fit, spacing_above: small, 96 | source: _/theme/milliner_logo.png, spacing_below: small, background: '', horizontal_align: center} 97 | name: image_logo 98 | layout_properties: {slot: logo} 99 | event_bindings: {mouse_down: image_logo_mouse_down} 100 | - type: LinearPanel 101 | properties: {} 102 | name: linear_panel_1 103 | layout_properties: {grid_position: 'YWEVWW,DNOXFE', slot: left-nav} 104 | components: 105 | - type: Link 106 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white, 107 | visible: true, text: Home, font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 108 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small, 109 | italic: false, background: '', bold: false, underline: false, icon: 'fa:home'} 110 | name: link_home 111 | layout_properties: {} 112 | event_bindings: {click: link_home_click} 113 | components: [] 114 | - type: Link 115 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white, 116 | visible: true, text: Users, font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 117 | spacing_above: small, icon_align: left, col_widths: '', spacing_below: small, 118 | italic: false, background: '', bold: false, underline: false, icon: 'fa:users'} 119 | name: link_users 120 | layout_properties: {} 121 | event_bindings: {click: link_users_click} 122 | - type: Link 123 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white, 124 | visible: true, text: Machines, font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 125 | spacing_above: small, icon_align: left, col_widths: '', spacing_below: small, 126 | italic: false, background: '', bold: false, underline: false, icon: 'fa:desktop'} 127 | name: link_machines 128 | layout_properties: {} 129 | event_bindings: {click: link_machines_click} 130 | - type: Link 131 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white, 132 | visible: true, text: Routes, font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 133 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small, 134 | italic: false, background: '', bold: false, underline: false, icon: 'fa:map-signs'} 135 | name: link_routes 136 | layout_properties: {} 137 | event_bindings: {click: link_routes_click} 138 | components: [] 139 | - type: Link 140 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: white, 141 | visible: true, text: Settings, font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 142 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small, 143 | italic: false, background: '', bold: false, underline: false, icon: 'fa:sliders'} 144 | name: link_settings 145 | layout_properties: {} 146 | components: 147 | - type: ColumnPanel 148 | properties: {col_widths: '{}'} 149 | name: column_panel_1 150 | layout_properties: {grid_position: 'RXAHIM,YGPBUR'} 151 | components: 152 | - type: Spacer 153 | properties: {height: 250} 154 | name: spacer_1 155 | layout_properties: {grid_position: 'ZEUGAL,EUIWPS'} 156 | - type: Label 157 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 158 | visible: true, text: '', font_size: 9, font: '', spacing_above: small, icon_align: left, 159 | spacing_below: small, italic: true, background: '', bold: false, underline: true, 160 | icon: ''} 161 | name: label_version 162 | layout_properties: {grid_position: 'ZYXVJI,VDCHIU'} 163 | data_bindings: [] 164 | event_bindings: {click: link_settings_click} 165 | -------------------------------------------------------------------------------- /client_code/Machines/MachineDetail/MachineRouteRow/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import MachineRouteRowTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class MachineRouteRow(MachineRouteRowTemplate): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings. 12 | self.init_components(**properties) 13 | self.label_online.text = self.item['machine']['online'] 14 | # Any code you write here will run before the form opens. 15 | -------------------------------------------------------------------------------- /client_code/Machines/MachineDetail/MachineRouteRow/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: {type: DataRowPanel} 2 | components: 3 | - type: Label 4 | properties: {} 5 | name: label_online 6 | layout_properties: {column: PGODDC} 7 | is_package: true 8 | -------------------------------------------------------------------------------- /client_code/Machines/MachineDetail/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import MachineDetailTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from ... import Startup 9 | 10 | class MachineDetail(MachineDetailTemplate): 11 | def __init__(self, id, machine_row, **properties): 12 | # Set Form properties and Data Bindings. 13 | self.url = Startup.url 14 | self.api_key = Startup.api_key 15 | self.item = anvil.server.call('get_machine_routes', self.url, self.api_key, id) 16 | self.machine = machine_row 17 | self.init_components(**properties) 18 | if self.item == {'routes': []}: 19 | self.column_panel_routes_container.clear() 20 | self.column_panel_routes_container.add_component(Label(text='This machine does not have any routes registered in Headscale.')) 21 | self.drop_down_users.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()] 22 | self.drop_down_users.selected_value = self.machine['user'] 23 | else: 24 | self.repeating_panel_machine_routes.items = self.item['routes'] 25 | self.drop_down_users.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()] 26 | self.drop_down_users.selected_value = self.machine['user'] 27 | # Any code you write here will run before the form opens. 28 | 29 | def button_update_machine_settings_click(self, **event_args): 30 | """This method is called when the button is clicked""" 31 | self.raise_event("x-close-alert", value=True) 32 | if self.text_box_machine_name.tag == True: 33 | anvil.server.call('rename_machine', self.url, self.api_key, self.machine['id'], self.text_box_machine_name.text) 34 | anvil.server.call('move_user', self.url, self. api_key, self.machine['id'], self.drop_down_users.selected_value) 35 | else: 36 | anvil.server.call('move_user', self.url, self. api_key, self.machine['id'], self.drop_down_users.selected_value) 37 | 38 | def text_box_machine_name_change(self, **event_args): 39 | """This method is called when the text in this text box is edited""" 40 | self.text_box_machine_name.tag = True 41 | 42 | def button_delete_machine_click(self, **event_args): 43 | """This method is called when the button is clicked""" 44 | answer = confirm('Are you sure you want to delete ' + self.machine['givenName']) 45 | if answer: 46 | anvil.server.call('delete_machine', self.url, self.api_key, self.machine['id']) 47 | self.machine.delete() 48 | self.raise_event("x-close-alert", value=True) 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /client_code/Machines/MachineDetail/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: GridPanel 6 | properties: {} 7 | name: grid_panel_container 8 | layout_properties: {grid_position: 'ONHNCD,BZYSAX'} 9 | components: 10 | - type: Label 11 | properties: {role: title, align: left, tooltip: '', border: '', foreground: '', 12 | visible: true, text: Routes, font_size: null, font: '', spacing_above: small, 13 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 14 | underline: true, icon: 'fa:map-signs'} 15 | name: label_route 16 | layout_properties: {row: NSJBFV, width_xs: 10, col_xs: 1, width: 803.328125} 17 | - type: ColumnPanel 18 | properties: {col_widths: '{}'} 19 | name: column_panel_routes_container 20 | layout_properties: {row: TISYJI, width_xs: 10, col_xs: 1} 21 | components: 22 | - type: DataGrid 23 | properties: 24 | role: null 25 | columns: 26 | - {id: YDQOGZ, title: Prefixes, data_key: prefix, $$hashKey: 'object:69729'} 27 | - {id: JPGVYI, title: Enabled, data_key: enabled, $$hashKey: 'object:69995'} 28 | - {id: PGODDC, title: Online, data_key: '', $$hashKey: 'object:69997'} 29 | - {id: IHGIYW, title: Advertised, data_key: advertised, $$hashKey: 'object:69999'} 30 | auto_header: true 31 | tooltip: '' 32 | border: '' 33 | foreground: '' 34 | rows_per_page: 20 35 | visible: true 36 | wrap_on: never 37 | show_page_controls: true 38 | spacing_above: small 39 | spacing_below: small 40 | background: '' 41 | name: data_grid_machine_routes 42 | layout_properties: {row: XUMFTA, width_xs: 10, col_xs: 1, width: 803.328125, 43 | grid_position: 'EKACBV,AAEKYR'} 44 | components: 45 | - type: RepeatingPanel 46 | properties: {spacing_above: none, spacing_below: none, item_template: Machines.MachineDetail.MachineRouteRow} 47 | name: repeating_panel_machine_routes 48 | layout_properties: {} 49 | data_bindings: [] 50 | - type: ColumnPanel 51 | properties: {col_widths: '{"YRYFIM":15,"VJHIWB":35,"DARJVK":15,"GMHCMT":35,"MPDJXC":30,"KNLIJN":30}'} 52 | name: column_panel_machine_settings 53 | layout_properties: {row: IDCJYU, width_xs: 7, col_xs: 1} 54 | components: 55 | - type: Label 56 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 57 | visible: true, text: 'Machine Name:', font_size: null, font: '', spacing_above: small, 58 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false, 59 | underline: false, icon: ''} 60 | name: label_machine_name 61 | layout_properties: {grid_position: 'NBSRYO,YRYFIM'} 62 | - type: TextBox 63 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: Type a new Machine Name, 64 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null, 65 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false, 66 | background: '', bold: false, underline: false} 67 | name: text_box_machine_name 68 | layout_properties: {grid_position: 'NBSRYO,VJHIWB'} 69 | event_bindings: {change: text_box_machine_name_change} 70 | - type: Button 71 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true, 72 | foreground: '', visible: true, text: Update, font_size: null, font: '', spacing_above: small, 73 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 74 | underline: false, icon: 'fa:floppy-o'} 75 | name: button_update_machine_settings 76 | layout_properties: {grid_position: 'NBSRYO,MPDJXC'} 77 | event_bindings: {click: button_update_machine_settings_click} 78 | - type: Label 79 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 80 | visible: true, text: 'Assigned User:', font_size: null, font: '', spacing_above: small, 81 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false, 82 | underline: false, icon: ''} 83 | name: label_machine_user 84 | layout_properties: {grid_position: 'MRMLUU,DARJVK'} 85 | - type: DropDown 86 | properties: {} 87 | name: drop_down_users 88 | layout_properties: {grid_position: 'MRMLUU,GMHCMT'} 89 | - type: Button 90 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true, 91 | foreground: '', visible: true, text: Delete, font_size: null, font: '', spacing_above: small, 92 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 93 | underline: false, icon: 'fa:trash'} 94 | name: button_delete_machine 95 | layout_properties: 96 | col_widths: {} 97 | grid_position: MRMLUU,KNLIJN 98 | event_bindings: {click: button_delete_machine_click} 99 | is_package: true 100 | -------------------------------------------------------------------------------- /client_code/Machines/MachineRow/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import MachineRowTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from ..MachineDetail import MachineDetail 9 | 10 | class MachineRow(MachineRowTemplate): 11 | def __init__(self, **properties): 12 | # Set Form properties and Data Bindings. 13 | self.init_components(**properties) 14 | 15 | # Any code you write here will run before the form opens. 16 | 17 | def link_machine_id_click(self, **event_args): 18 | """This method is called when the link is clicked""" 19 | update = alert(MachineDetail(self.item['id'], self.item), large=True, title=self.item['name'], buttons=[]) 20 | if update: 21 | self.parent.raise_event('x-refresh') 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /client_code/Machines/MachineRow/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: {type: DataRowPanel} 2 | components: 3 | - type: Link 4 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: '', 5 | visible: true, text: '', font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 6 | spacing_above: small, icon_align: left, col_widths: '', spacing_below: small, 7 | italic: false, background: '', bold: false, underline: false, icon: 'fa:desktop'} 8 | name: link_machine_id 9 | layout_properties: {column: RWBSWP} 10 | data_bindings: 11 | - {property: text, code: 'self.item[''id'']'} 12 | event_bindings: {click: link_machine_id_click} 13 | is_package: true 14 | -------------------------------------------------------------------------------- /client_code/Machines/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import MachinesTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from .. import Startup 9 | 10 | class Machines(MachinesTemplate): 11 | def __init__(self, **properties): 12 | self.url = Startup.url 13 | self.api_key = Startup.api_key 14 | self.item = anvil.server.call('get_machine_table') 15 | self.repeating_panel_machines.set_event_handler('x-refresh', self.refresh_data) 16 | self.init_components(**properties) 17 | self.repeating_panel_machines.items = self.item.search() 18 | self.drop_down_user.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()] 19 | # Any code you write here will run before the form opens. 20 | 21 | def refresh_data(self, **event_args): 22 | anvil.server.call('record_machines') 23 | self.item = anvil.server.call('get_machine_table') 24 | self.repeating_panel_machines.items = self.item.search() 25 | 26 | def button_add_machine_click(self, **event_args): 27 | """This method is called when the button is clicked""" 28 | self.column_panel_add_machine.visible = True 29 | 30 | def button_save_new_machine_click(self, **event_args): 31 | """This method is called when the button is clicked""" 32 | alert(anvil.server.call('register_machine', self.url, self.api_key, self.text_box_nodekey.text, self.drop_down_user.selected_value)) 33 | self.refresh_data_bindings() 34 | 35 | def button_cancel_click(self, **event_args): 36 | """This method is called when the button is clicked""" 37 | self.column_panel_add_machine.visible = False 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /client_code/Machines/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true, 4 | wrap_on: mobile, col_spacing: tiny, spacing_above: small, col_widths: '{}', spacing_below: small, 5 | background: ''} 6 | components: 7 | - type: form:Home.Header 8 | properties: {} 9 | name: header_1 10 | layout_properties: {grid_position: 'SQMKIS,LVQUDV', full_width_row: true} 11 | - type: Label 12 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '', 13 | visible: true, text: Machines, font_size: null, font: '', spacing_above: none, 14 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false, 15 | underline: true, icon: 'fa:desktop'} 16 | name: label_title 17 | layout_properties: {grid_position: 'KJKRCR,AFPXXY'} 18 | - type: DataGrid 19 | properties: 20 | role: null 21 | columns: 22 | - {id: RWBSWP, title: Id, data_key: id, $$hashKey: 'object:686', width: '60', 23 | expand: false} 24 | - {id: IYECDI, title: Name, data_key: givenName, $$hashKey: 'object:672', width: '100', 25 | expand: true} 26 | - {id: UAIMLQ, title: IpAddr, data_key: ipAddr, $$hashKey: 'object:673', expand: false, 27 | width: '150'} 28 | - {id: HLQAWD, title: User, data_key: user, $$hashKey: 'object:685', expand: false, 29 | width: '90'} 30 | - {id: KMRUBB, title: Expiry, data_key: expiry, $$hashKey: 'object:675', expand: true, 31 | width: '175'} 32 | - {id: ATNRNG, title: CreatedAt, data_key: createdAt, $$hashKey: 'object:678', 33 | expand: true, width: '175'} 34 | - {id: WCOZKP, title: Online, data_key: online, $$hashKey: 'object:688', width: '75', 35 | expand: false} 36 | - {id: MHXUUT, title: LastUpdate, data_key: lastSuccessfulUpdate, $$hashKey: 'object:680', 37 | expand: true, width: '175'} 38 | - {id: WWESIH, title: LastSeen, data_key: lastSeen, $$hashKey: 'object:683', expand: true, 39 | width: '175'} 40 | auto_header: true 41 | tooltip: '' 42 | border: '' 43 | foreground: '' 44 | rows_per_page: 20 45 | visible: true 46 | wrap_on: never 47 | show_page_controls: true 48 | spacing_above: small 49 | spacing_below: small 50 | background: '' 51 | name: data_grid_machines 52 | layout_properties: {grid_position: 'NDGYIH,QPSALL', full_width_row: true} 53 | components: 54 | - type: RepeatingPanel 55 | properties: {role: null, tooltip: '', border: '', foreground: '', items: null, 56 | visible: true, spacing_above: none, spacing_below: none, item_template: Machines.MachineRow, 57 | background: ''} 58 | name: repeating_panel_machines 59 | layout_properties: {} 60 | data_bindings: [] 61 | - type: GridPanel 62 | properties: {spacing_above: small, spacing_below: none, background: '', foreground: '', 63 | border: '', visible: true, role: null, tooltip: ''} 64 | name: grid_panel_1 65 | layout_properties: {slot: footer} 66 | components: 67 | - type: Button 68 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 69 | foreground: '', visible: true, text: Add Machine, font_size: null, font: '', 70 | spacing_above: small, icon_align: left, spacing_below: none, italic: false, 71 | background: '', bold: false, underline: false, icon: 'fa:desktop'} 72 | name: button_add_machine 73 | layout_properties: {row: UCOPAM, width_xs: 3, col_xs: 0, width: 216.84375} 74 | event_bindings: {click: button_add_machine_click} 75 | - type: ColumnPanel 76 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: false, 77 | wrap_on: mobile, col_spacing: medium, spacing_above: small, col_widths: '{"RANRWU":10,"ZZSNKO":70,"GEFRIH":5,"WGLSKD":20,"NTPOIQ":10,"XEBUHN":10}', 78 | spacing_below: small, background: ''} 79 | name: column_panel_add_machine 80 | layout_properties: {row: ETSIAM, width_xs: 12, col_xs: 0, width: 933.40625} 81 | components: 82 | - type: Label 83 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 84 | visible: true, text: 'NodeKey:', font_size: null, font: '', spacing_above: none, 85 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false, 86 | underline: false, icon: ''} 87 | name: label_1 88 | layout_properties: {grid_position: 'CXZNMP,RANRWU'} 89 | - type: TextBox 90 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: '', 91 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null, 92 | font: '', spacing_above: none, type: text, spacing_below: none, italic: false, 93 | background: '', bold: false, underline: false} 94 | name: text_box_nodekey 95 | layout_properties: {grid_position: 'CXZNMP,ZZSNKO'} 96 | event_bindings: {} 97 | - type: Spacer 98 | properties: {height: 32} 99 | name: spacer_1 100 | layout_properties: {grid_position: 'CXZNMP,XEBUHN'} 101 | - type: Label 102 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 103 | visible: true, text: 'User:', font_size: null, font: '', spacing_above: none, 104 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false, 105 | underline: false, icon: ''} 106 | name: label_user 107 | layout_properties: {grid_position: 'IVFPJK,GEFRIH'} 108 | - type: DropDown 109 | properties: 110 | role: null 111 | align: full 112 | tooltip: '' 113 | placeholder: '' 114 | border: '' 115 | enabled: true 116 | foreground: '' 117 | items: [] 118 | visible: true 119 | font_size: null 120 | font: '' 121 | spacing_above: none 122 | spacing_below: none 123 | italic: false 124 | background: '' 125 | bold: false 126 | underline: false 127 | include_placeholder: false 128 | name: drop_down_user 129 | layout_properties: {grid_position: 'IVFPJK,ZCFRPW'} 130 | event_bindings: {} 131 | - type: Button 132 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true, 133 | foreground: '', visible: true, text: Cancel, font_size: null, font: '', 134 | spacing_above: none, icon_align: left, spacing_below: none, italic: false, 135 | background: '', bold: false, underline: false, icon: 'fa:times-circle'} 136 | name: button_cancel 137 | layout_properties: {row: UCOPAM, width_xs: 2, col_xs: 3, width: 137.734375, 138 | grid_position: 'IVFPJK,WGLSKD'} 139 | event_bindings: {click: button_cancel_click} 140 | - type: Button 141 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 142 | foreground: '', visible: true, text: Save, font_size: null, font: '', spacing_above: none, 143 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 144 | underline: false, icon: 'fa:floppy-o'} 145 | name: button_save_new_machine 146 | layout_properties: {grid_position: 'IVFPJK,NTPOIQ'} 147 | event_bindings: {click: button_save_new_machine_click} 148 | is_package: true 149 | -------------------------------------------------------------------------------- /client_code/Routes/RouteRow/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import RouteRowTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from ... import Startup 9 | 10 | class RouteRow(RouteRowTemplate): 11 | def __init__(self, **properties): 12 | # Set Form properties and Data Bindings. 13 | self.init_components(**properties) 14 | 15 | # Any code you write here will run before the form opens. 16 | 17 | def check_box_enabled_change(self, **event_args): 18 | """This method is called when this checkbox is checked or unchecked""" 19 | self.url = Startup.url 20 | self.api_key = Startup.api_key 21 | anvil.server.call('update_route', self.url, self.api_key, self.item['id'], self.check_box_enabled.checked) 22 | anvil.server.call('record_routes') 23 | self.parent.raise_event('x-refresh') 24 | 25 | -------------------------------------------------------------------------------- /client_code/Routes/RouteRow/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: {type: DataRowPanel} 2 | components: 3 | - type: CheckBox 4 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, foreground: '', 5 | allow_indeterminate: false, visible: true, text: '', font_size: null, font: '', 6 | spacing_above: small, spacing_below: small, italic: false, background: '', bold: false, 7 | checked: false, underline: false} 8 | name: check_box_enabled 9 | layout_properties: {column: CHTJAW} 10 | data_bindings: 11 | - {property: checked, code: 'self.item[''enabled'']', writeback: false} 12 | event_bindings: {change: check_box_enabled_change} 13 | is_package: true 14 | -------------------------------------------------------------------------------- /client_code/Routes/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import RoutesTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class Routes(RoutesTemplate): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings. 12 | self.item = anvil.server.call('get_routes_tables') 13 | self.init_components(**properties) 14 | self.repeating_panel_routes.add_event_handler('x-refresh', self.refresh_data) 15 | self.repeating_panel_routes.items = self.item.search() 16 | # Any code you write here will run before the form opens. 17 | 18 | def refresh_data(self, **event_args): 19 | self.refresh_data_bindings() 20 | self.repeating_panel_routes.items = self.item.search() 21 | -------------------------------------------------------------------------------- /client_code/Routes/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: form:Home.Header 6 | properties: {} 7 | name: header_1 8 | layout_properties: {grid_position: 'DPISDB,BZBANN', full_width_row: true} 9 | - type: Label 10 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '', 11 | visible: true, text: Routes, font_size: null, font: '', spacing_above: none, icon_align: left, 12 | spacing_below: none, italic: false, background: '', bold: false, underline: true, 13 | icon: 'fa:map-signs'} 14 | name: label_1 15 | layout_properties: {grid_position: 'QZOCQY,ASMRMC'} 16 | - type: DataGrid 17 | properties: 18 | role: null 19 | columns: 20 | - {id: OJSHAX, title: Id, data_key: id, $$hashKey: 'object:1718', width: '50'} 21 | - {id: WJJKDO, title: Prefix, data_key: prefix, $$hashKey: 'object:1719', width: '175', 22 | expand: false} 23 | - {id: RQPBKG, title: Machine Name, data_key: givenName, $$hashKey: 'object:1720', 24 | width: '125', expand: true} 25 | - {id: DEYOGZ, title: MachineIPs, data_key: machineIPs, $$hashKey: 'object:1721', 26 | width: '175', expand: true} 27 | - {id: CHTJAW, title: Enabled, data_key: enabled, $$hashKey: 'object:1722', width: '90'} 28 | auto_header: true 29 | tooltip: '' 30 | border: '' 31 | foreground: '' 32 | rows_per_page: 20 33 | visible: true 34 | wrap_on: never 35 | show_page_controls: true 36 | spacing_above: small 37 | spacing_below: small 38 | background: '' 39 | name: data_grid_routes 40 | layout_properties: {grid_position: 'YDXYZE,QBBMJZ'} 41 | components: 42 | - type: RepeatingPanel 43 | properties: {spacing_above: none, spacing_below: none, item_template: Routes.RouteRow} 44 | name: repeating_panel_routes 45 | layout_properties: {} 46 | is_package: true 47 | -------------------------------------------------------------------------------- /client_code/Settings/AddUser/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import AddUserTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class AddUser(AddUserTemplate): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings. 12 | self.item['email'] = '' 13 | self.item['password'] = '' 14 | self.item['enabled'] = '' 15 | self.init_components(**properties) 16 | self.check_box_enabled.checked = True 17 | self.item['enabled'] = True 18 | 19 | 20 | # Any code you write here will run before the form opens. 21 | 22 | 23 | -------------------------------------------------------------------------------- /client_code/Settings/AddUser/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: GridPanel 6 | properties: {} 7 | name: grid_panel_container 8 | layout_properties: {grid_position: 'TRDWYQ,GUIINP'} 9 | components: 10 | - type: ColumnPanel 11 | properties: {role: card, col_widths: '{"XIGHDZ":15,"HNDHCK":45,"TBHJJZ":15,"VFIWJQ":45}'} 12 | name: card_1 13 | layout_properties: {row: DDPLGN, width_xs: 6, col_xs: 3} 14 | components: 15 | - type: Label 16 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '', 17 | visible: true, text: Add a User, font_size: null, font: '', spacing_above: small, 18 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 19 | underline: true, icon: 'fa:users'} 20 | name: label_1 21 | layout_properties: {grid_position: 'TWHDCI,KOUNMP'} 22 | - type: Label 23 | properties: {role: null, align: center, tooltip: '', border: '', foreground: '', 24 | visible: true, text: 'Email:', font_size: null, font: '', spacing_above: small, 25 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false, 26 | underline: false, icon: ''} 27 | name: label_email 28 | layout_properties: {grid_position: 'VQZNRK,XIGHDZ'} 29 | - type: TextBox 30 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: '', 31 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null, 32 | font: '', spacing_above: small, type: email, spacing_below: small, italic: false, 33 | background: '', bold: false, underline: false} 34 | name: text_box_email 35 | layout_properties: {grid_position: 'VQZNRK,HNDHCK'} 36 | data_bindings: 37 | - {property: text, code: 'self.item[''email'']', writeback: true} 38 | - type: Label 39 | properties: {role: null, align: center, tooltip: '', border: '', foreground: '', 40 | visible: true, text: 'Password:', font_size: null, font: '', spacing_above: small, 41 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false, 42 | underline: false, icon: ''} 43 | name: label_2 44 | layout_properties: {grid_position: 'AFBOQG,TBHJJZ'} 45 | - type: TextBox 46 | properties: {role: null, align: left, hide_text: true, tooltip: '', placeholder: '', 47 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null, 48 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false, 49 | background: '', bold: false, underline: false} 50 | name: text_box_password 51 | layout_properties: {grid_position: 'AFBOQG,VFIWJQ'} 52 | data_bindings: 53 | - {property: text, code: 'self.item[''password'']', writeback: true} 54 | - type: CheckBox 55 | properties: {role: null, align: center, tooltip: '', border: '', enabled: true, 56 | foreground: '', allow_indeterminate: false, visible: true, text: Enable user, 57 | font_size: null, font: '', spacing_above: small, spacing_below: small, italic: false, 58 | background: '', bold: false, checked: true, underline: false} 59 | name: check_box_enabled 60 | layout_properties: {grid_position: 'ERSFCE,BREHIQ'} 61 | data_bindings: 62 | - {property: checked, code: 'self.item[''enabled'']', writeback: true} 63 | event_bindings: {} 64 | is_package: true 65 | -------------------------------------------------------------------------------- /client_code/Settings/AppUsers/AppUsersTemplate/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import AppUsersTemplateTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class AppUsersTemplate(AppUsersTemplateTemplate): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings. 12 | self.set_event_handler('x-refresh-child', self.refresh_data) 13 | self.init_components(**properties) 14 | 15 | # Any code you write here will run before the form opens. 16 | def refresh_data(self, **event_args): 17 | self.data_row_panel_view.item = self.item 18 | 19 | def button_edit_click(self, **event_args): 20 | """This method is called when the button is clicked""" 21 | self.data_row_panel_view.visible = False 22 | self.data_row_panel_edit.visible = True 23 | 24 | def button_save_user_edit_click(self, **event_args): 25 | """This method is called when the button is clicked""" 26 | if self.text_box_password.text == '': 27 | anvil.server.call('update_app_user', self.text_box_email.text, self.item, False, None) 28 | else: 29 | anvil.server.call('update_app_user', self.text_box_email.text, self.item, True, self.text_box_password.text) 30 | self.parent.raise_event('x-refresh') 31 | self.data_row_panel_view.visible = True 32 | self.data_row_panel_edit.visible = False 33 | 34 | def button_delete_user_click(self, **event_args): 35 | """This method is called when the button is clicked""" 36 | delete_user = alert('Are you SURE you want to DELETE ' + self.item['email'], buttons=[('Yes', True), ('No', False)]) 37 | if delete_user: 38 | with Notification('Deleted User ' + self.item['email'], style='info'): 39 | self.item.delete() 40 | self.parent.raise_event('x-refresh') 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /client_code/Settings/AppUsers/AppUsersTemplate/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: DataRowPanel 3 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: false, 4 | visible: true, font_size: null, font: '', spacing_above: none, spacing_below: none, 5 | italic: false, background: '', bold: false, underline: false} 6 | data_bindings: [] 7 | components: 8 | - type: DataRowPanel 9 | properties: {} 10 | name: data_row_panel_view 11 | layout_properties: {column: null} 12 | data_bindings: 13 | - {property: item, code: self.item} 14 | components: 15 | - type: Label 16 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 17 | visible: true, text: '************************', font_size: null, font: '', 18 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 19 | background: '', bold: false, underline: false, icon: ''} 20 | name: label_password 21 | layout_properties: {column: NJORLQ} 22 | - type: CheckBox 23 | properties: {role: null, align: left, tooltip: '', border: '', enabled: false, 24 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null, 25 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '', 26 | bold: false, checked: false, underline: false} 27 | name: check_box_view_enabled 28 | layout_properties: {column: XLHPBD} 29 | data_bindings: 30 | - {property: checked, code: 'self.item[''enabled'']', writeback: false} 31 | - type: CheckBox 32 | properties: {role: null, align: left, tooltip: '', border: '', enabled: false, 33 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null, 34 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '', 35 | bold: false, checked: false, underline: false} 36 | name: check_box_view_conf_email 37 | layout_properties: {column: PZYHXE} 38 | data_bindings: 39 | - {property: checked, code: 'self.item[''confirmed_email'']', writeback: false} 40 | - type: Button 41 | properties: {role: null, align: center, tooltip: '', border: '', enabled: true, 42 | foreground: '', visible: true, text: '', font_size: null, font: '', spacing_above: small, 43 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 44 | underline: false, icon: 'fa:pencil'} 45 | name: button_edit 46 | layout_properties: {column: QFPEZE} 47 | event_bindings: {click: button_edit_click} 48 | - type: DataRowPanel 49 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: true, 50 | visible: false, item: null, font_size: null, font: '', spacing_above: none, spacing_below: none, 51 | italic: false, background: '', bold: false, underline: false} 52 | name: data_row_panel_edit 53 | layout_properties: {column: null} 54 | data_bindings: 55 | - {property: item, code: self.item} 56 | components: 57 | - type: Button 58 | properties: {role: null, align: center, tooltip: '', border: '', enabled: true, 59 | foreground: '', visible: true, text: '', font_size: null, font: '', spacing_above: small, 60 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 61 | underline: false, icon: 'fa:floppy-o'} 62 | name: button_save_user_edit 63 | layout_properties: {column: QFPEZE} 64 | event_bindings: {click: button_save_user_edit_click} 65 | - type: TextBox 66 | properties: {role: null, align: left, hide_text: true, tooltip: '', placeholder: '********************', 67 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null, 68 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false, 69 | background: '', bold: false, underline: false} 70 | name: text_box_password 71 | layout_properties: {column: NJORLQ} 72 | event_bindings: {} 73 | data_bindings: [] 74 | - type: CheckBox 75 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, 76 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null, 77 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '', 78 | bold: false, checked: false, underline: false} 79 | name: check_box_edit_enabled 80 | layout_properties: {column: XLHPBD} 81 | data_bindings: 82 | - {property: checked, code: 'self.item[''enabled'']', writeback: true} 83 | - type: CheckBox 84 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, 85 | foreground: '', allow_indeterminate: false, visible: true, text: '', font_size: null, 86 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '', 87 | bold: false, checked: false, underline: false} 88 | name: check_box_edit_confirmed_email 89 | layout_properties: {column: PZYHXE} 90 | data_bindings: 91 | - {property: checked, code: 'self.item[''confirmed_email'']', writeback: true} 92 | - type: TextBox 93 | properties: {} 94 | name: text_box_email 95 | layout_properties: {column: EWLQOA} 96 | event_bindings: {} 97 | data_bindings: 98 | - {property: text, code: 'self.item[''email'']', writeback: false} 99 | - type: Button 100 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 101 | foreground: '', visible: true, text: Delete Milliner User, font_size: null, 102 | font: '', spacing_above: small, icon_align: left, spacing_below: small, italic: false, 103 | background: '', bold: false, underline: false, icon: 'fa:trash'} 104 | name: button_delete_user 105 | layout_properties: {column: null} 106 | event_bindings: {click: button_delete_user_click} 107 | is_package: true 108 | -------------------------------------------------------------------------------- /client_code/Settings/AppUsers/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import AppUsersTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from ..AddUser import AddUser 8 | from anvil.tables import app_tables 9 | 10 | class AppUsers(AppUsersTemplate): 11 | def __init__(self, **properties): 12 | # Set Form properties and Data Bindings. 13 | self.repeating_panel_appusers.set_event_handler('x-refresh', self.refresh_data) 14 | self.users = {} 15 | self.item = anvil.server.call('get_app_users_table').search() 16 | self.init_components(**properties) 17 | 18 | # Any code you write here will run before the form opens. 19 | 20 | def refresh_data(self, **event_args): 21 | self.item = anvil.server.call('get_app_users_table').search() 22 | 23 | def button_add_user_click(self, **event_args): 24 | """This method is called when the button is clicked""" 25 | save = alert(AddUser(item=self.users), buttons=[('Save', True), ('Cancel', False)], large=True) 26 | if save: 27 | with Notification('Adding User ' + self.users['email'], timeout=3, style='success'): 28 | anvil.server.call('add_appuser', self.users['email'], self.users['enabled'], True, self.users['password']) 29 | self.refresh_data() 30 | 31 | -------------------------------------------------------------------------------- /client_code/Settings/AppUsers/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: form:Home.Header 6 | properties: {} 7 | name: header_1 8 | layout_properties: {grid_position: 'JDDFWY,VKEPGO'} 9 | - type: DataGrid 10 | properties: 11 | role: null 12 | columns: 13 | - {id: XLHPBD, title: Enabled, data_key: enabled, $$hashKey: 'object:224149', 14 | width: '80'} 15 | - {id: EWLQOA, title: Email, data_key: email, $$hashKey: 'object:224148', width: '175', 16 | expand: true} 17 | - {id: NJORLQ, title: Password, data_key: password, $$hashKey: 'object:224164', 18 | width: '175', expand: true} 19 | - {id: BJTNIH, title: Last Login, data_key: last_login, $$hashKey: 'object:224150', 20 | width: '175', expand: true} 21 | - {id: ADBALN, title: Password Failures, data_key: n_password_failures, $$hashKey: 'object:224152', 22 | width: 100, expand: false} 23 | - {id: PZYHXE, title: Confirmed Email, data_key: confirmed_email, $$hashKey: 'object:224153', 24 | width: 90, expand: false} 25 | - {id: QFPEZE, title: '', data_key: '', $$hashKey: 'object:230262', width: '75', 26 | expand: false} 27 | auto_header: true 28 | tooltip: '' 29 | border: '' 30 | foreground: '' 31 | rows_per_page: 20 32 | visible: true 33 | wrap_on: never 34 | show_page_controls: true 35 | spacing_above: small 36 | spacing_below: small 37 | background: '' 38 | name: data_grid_appusers 39 | layout_properties: {grid_position: 'GDBZFO,EBQUII'} 40 | components: 41 | - type: RepeatingPanel 42 | properties: {role: null, tooltip: '', border: '', foreground: '', items: null, 43 | visible: true, spacing_above: none, spacing_below: none, item_template: Settings.AppUsers.AppUsersTemplate, 44 | background: ''} 45 | name: repeating_panel_appusers 46 | layout_properties: {} 47 | data_bindings: 48 | - {property: items, code: self.item} 49 | - type: GridPanel 50 | properties: {} 51 | name: grid_panel_footer 52 | layout_properties: {slot: footer} 53 | components: 54 | - type: Button 55 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 56 | foreground: '', visible: true, text: Add Milliner User, font_size: null, font: '', 57 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 58 | background: '', bold: false, underline: false, icon: 'fa:users'} 59 | name: button_add_user 60 | layout_properties: {row: FFDUTA, width_xs: 4, col_xs: 0, width: 184.34375} 61 | event_bindings: {click: button_add_user_click} 62 | is_package: true 63 | -------------------------------------------------------------------------------- /client_code/Settings/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import SettingsTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from .AppUsers import AppUsers 9 | from .. import Startup 10 | 11 | class Settings(SettingsTemplate): 12 | def __init__(self, **properties): 13 | self.users = anvil.server.call('get_app_users_table').search() 14 | self.url = Startup.url 15 | self.api_key = Startup.api_key 16 | # Set Form properties and Data Bindings. 17 | self.init_components(**properties) 18 | 19 | # Any code you write here will run before the form opens. 20 | 21 | def button_save_click(self, **event_args): 22 | """This method is called when the button is clicked""" 23 | if app_tables.settings.get() == None: 24 | app_tables.settings.add_row(url=self.url, api_key=self.api_key) 25 | self.refresh_data_bindings() 26 | else: 27 | settings_row = app_tables.settings.get() 28 | settings_row.update(url=self.text_box_url.text, api_key=self.text_box_api_key.text) 29 | self.refresh_data_bindings() 30 | 31 | def button_test_api_click(self, **event_args): 32 | """This method is called when the button is clicked""" 33 | try: 34 | status_returned = anvil.server.call('test_api_key', url=self.url, api_key=self.api_key) 35 | except: 36 | raise Exception('No URL has been saved in Settings') 37 | if status_returned == 200: 38 | status = f'{status_returned} - Connection Succesful' 39 | else: 40 | status = f'Unable to connect - {status_returned}. Please check your API key, URL, and CORS headers' 41 | alert(status, title="API Test Results") 42 | 43 | def button_app_users_click(self, **event_args): 44 | """This method is called when the button is clicked""" 45 | self.clear() 46 | self.add_component(AppUsers()) 47 | 48 | def button_renew_api_key_click(self, **event_args): 49 | """This method is called when the button is clicked""" 50 | result, response, new_api_key = anvil.server.call('renew_api_key', url=self.url, api_key=self.api_key) 51 | if result == True and response == 'Key updated and validated': 52 | anvil.server.call('update_api_key_settings', new_api_key) 53 | self.api_key = [r['api_key'] for r in app_tables.settings.search()][0] 54 | Notification(response, timeout=3).show() 55 | elif result == True: 56 | Notification(response, timeout=3).show() 57 | elif result == False: 58 | Notification(response, timeout=3).show() 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /client_code/Settings/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: form:Home.Header 6 | properties: {} 7 | name: header_1 8 | layout_properties: {grid_position: 'MNOOSN,PVTMSV', full_width_row: true} 9 | - type: ColumnPanel 10 | properties: {col_widths: '{}'} 11 | name: column_panel_settings 12 | layout_properties: {grid_position: 'NGXVPF,MZNVVM'} 13 | components: 14 | - type: GridPanel 15 | properties: {spacing_above: small, spacing_below: small, background: '', foreground: '', 16 | border: '', visible: true, role: null, tooltip: ''} 17 | name: grid_panel_1 18 | layout_properties: {grid_position: 'EILTKL,NMJNVE'} 19 | components: 20 | - type: Label 21 | properties: {role: title, align: left, tooltip: '', border: '', foreground: '', 22 | visible: true, text: 'Headscale Server Settings:', font_size: null, font: '', 23 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 24 | background: '', bold: false, underline: true, icon: ''} 25 | name: label_header1 26 | layout_properties: {row: SBYKCF, width_xs: 12, col_xs: 0, width: 750} 27 | - type: Label 28 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 29 | visible: true, text: 'Headscale Server URL:', font_size: null, font: '', spacing_above: small, 30 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false, 31 | underline: false, icon: ''} 32 | name: label_url 33 | layout_properties: {row: QYHVSU, width_xs: 2, col_xs: 0, width: 136.65625} 34 | - type: TextBox 35 | properties: {} 36 | name: text_box_url 37 | layout_properties: {row: QYHVSU, width_xs: 9, col_xs: 2} 38 | data_bindings: 39 | - {property: text, code: self.url, writeback: true} 40 | - type: Label 41 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', 42 | visible: true, text: 'Headscale API Key:', font_size: null, font: '', spacing_above: small, 43 | icon_align: left, spacing_below: small, italic: true, background: '', bold: false, 44 | underline: false, icon: ''} 45 | name: label_api_key 46 | layout_properties: {row: FGOVGD, width_xs: 2, col_xs: 0, width: 136.65625} 47 | - type: TextBox 48 | properties: {} 49 | name: text_box_api_key 50 | layout_properties: {row: FGOVGD, width_xs: 9, col_xs: 2} 51 | data_bindings: 52 | - {property: text, code: self.api_key, writeback: true} 53 | - type: Button 54 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 55 | foreground: '', visible: true, text: Renew API Key, font_size: null, font: '', 56 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 57 | background: '', bold: false, underline: false, icon: 'fa:refresh'} 58 | name: button_renew_api_key 59 | layout_properties: {row: GILCGU, width_xs: 3, col_xs: 0, width: 229.988} 60 | event_bindings: {click: button_renew_api_key_click} 61 | - type: Button 62 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 63 | foreground: '', visible: true, text: Test API, font_size: null, font: '', 64 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 65 | background: '', bold: false, underline: false, icon: 'fa:cog'} 66 | name: button_test_api 67 | layout_properties: {row: GILCGU, width_xs: 2, col_xs: 3, width: 99.9875} 68 | event_bindings: {click: button_test_api_click} 69 | - type: Button 70 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true, 71 | foreground: '', visible: true, text: Save, font_size: null, font: '', spacing_above: small, 72 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 73 | underline: false, icon: 'fa:floppy-o'} 74 | name: button_save 75 | layout_properties: {row: GILCGU, width_xs: 2, col_xs: 9, width: 136.65625} 76 | event_bindings: {click: button_save_click} 77 | - type: Label 78 | properties: {role: title, align: left, tooltip: '', border: '', foreground: '', 79 | visible: true, text: 'Milliner Settings:', font_size: null, font: '', spacing_above: small, 80 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 81 | underline: true, icon: ''} 82 | name: label_app_users 83 | layout_properties: {row: FMZJBQ, width_xs: 12, col_xs: 0, width: 750} 84 | - type: Button 85 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 86 | foreground: '', visible: true, text: Milliner Users, font_size: null, font: '', 87 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 88 | background: '', bold: false, underline: false, icon: 'fa:users'} 89 | name: button_app_users 90 | layout_properties: {row: YCCUCA, width_xs: 3, col_xs: 0, width: 165} 91 | event_bindings: {click: button_app_users_click} 92 | is_package: true 93 | -------------------------------------------------------------------------------- /client_code/Startup.py: -------------------------------------------------------------------------------- 1 | import anvil.server 2 | import anvil.users 3 | import anvil.tables as tables 4 | import anvil.tables.query as q 5 | from anvil.tables import app_tables 6 | from anvil import * 7 | 8 | def fresh_install(): 9 | fresh_install = anvil.server.call('check_fresh_install') 10 | if not fresh_install['settings_exists']: 11 | url = '' 12 | api_key = '' 13 | return url, api_key 14 | elif fresh_install['settings_exists']: 15 | url = [r['url'] for r in app_tables.settings.search()][0] 16 | api_key = [r['api_key'] for r in app_tables.settings.search()][0] 17 | return url, api_key 18 | 19 | def error_handler(err): 20 | alert(str(err), title="An error has occurred", large=True, dismissible=False) 21 | # 0.1.4 - Headscale API updated machines to nodes, basic API re-write completed. 22 | version = 'v.0.1.4' 23 | url, api_key = fresh_install() 24 | user = anvil.users.get_user() 25 | set_default_error_handling(error_handler) 26 | 27 | def startup(): 28 | user = anvil.users.login_with_form() 29 | open_form('Home') 30 | 31 | if __name__ == "__main__": 32 | startup() 33 | 34 | -------------------------------------------------------------------------------- /client_code/Users/PreAuth/GenerateKey/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import GenerateKeyTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class GenerateKey(GenerateKeyTemplate): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings 12 | self.init_components(**properties) 13 | self.drop_down_users.items = [r['name'] for r in anvil.server.call('get_hs_users_table').search()] 14 | 15 | # Any code you write here will run before the form opens. 16 | 17 | def check_box_ephmeral_change(self, **event_args): 18 | """This method is called when this checkbox is checked or unchecked""" 19 | if self.check_box_ephmeral.checked: 20 | self.item['is_ephemeral'] = 'True' 21 | else: 22 | self.item['is_ephemeral'] = 'False' 23 | 24 | #def check_box_reusable_change(self, **event_args): 25 | # """This method is called when this checkbox is checked or unchecked""" 26 | #if self.check_box_reusable.checked: 27 | #self.item['is_reusable'] = 'True' 28 | #else: 29 | #self.item['is_reusable'] = 'False' 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /client_code/Users/PreAuth/GenerateKey/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | data_bindings: [] 5 | components: 6 | - type: GridPanel 7 | properties: {} 8 | name: grid_panel_1 9 | layout_properties: {grid_position: 'SNKWKX,YNQQYM'} 10 | components: 11 | - type: DropDown 12 | properties: 13 | role: null 14 | align: full 15 | tooltip: '' 16 | placeholder: Select User 17 | border: '' 18 | enabled: true 19 | foreground: '' 20 | items: [] 21 | visible: true 22 | font_size: null 23 | font: '' 24 | spacing_above: small 25 | spacing_below: small 26 | italic: false 27 | background: '' 28 | bold: false 29 | underline: false 30 | include_placeholder: true 31 | name: drop_down_users 32 | layout_properties: {grid_position: 'FAOQXD,ZRWIGG', row: GQYFHH, width_xs: 12, 33 | col_xs: 0} 34 | data_bindings: 35 | - {property: selected_value, code: 'self.item[''selected_user'']', writeback: true} 36 | event_bindings: {} 37 | - type: DropDown 38 | properties: 39 | role: null 40 | align: full 41 | tooltip: '' 42 | placeholder: '' 43 | border: '' 44 | enabled: true 45 | foreground: '' 46 | items: ['7', '15', '30', '45', '90'] 47 | visible: true 48 | font_size: null 49 | font: '' 50 | spacing_above: small 51 | spacing_below: small 52 | italic: false 53 | background: '' 54 | bold: false 55 | underline: false 56 | include_placeholder: false 57 | name: drop_down_timeout 58 | layout_properties: {row: UXZNJQ, width_xs: 12, col_xs: 0, width: 970} 59 | data_bindings: 60 | - {property: selected_value, code: 'self.item[''expiration_date'']', writeback: true} 61 | - type: CheckBox 62 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, 63 | foreground: '', allow_indeterminate: false, visible: true, text: Ephemeral, 64 | font_size: null, font: '', spacing_above: small, spacing_below: small, italic: false, 65 | background: '', bold: false, checked: false, underline: false} 66 | name: check_box_ephemeral 67 | layout_properties: {row: RKWCJY, width_xs: 1, col_xs: 1, width: 136.667} 68 | data_bindings: 69 | - {property: checked, code: 'self.item[''is_ephemeral'']', writeback: true} 70 | event_bindings: {} 71 | - type: CheckBox 72 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, 73 | foreground: '', allow_indeterminate: false, visible: true, text: Reusable, font_size: null, 74 | font: '', spacing_above: small, spacing_below: small, italic: false, background: '', 75 | bold: false, checked: false, underline: false} 76 | name: check_box_reusable 77 | layout_properties: {row: RKWCJY, width_xs: 1, col_xs: 5, width: 136.667} 78 | data_bindings: 79 | - {property: checked, code: 'self.item[''is_reusable'']', writeback: true} 80 | event_bindings: {} 81 | is_package: true 82 | -------------------------------------------------------------------------------- /client_code/Users/PreAuth/PreAuthKeyRow/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import PreAuthKeyRowTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | 9 | class PreAuthKeyRow(PreAuthKeyRowTemplate): 10 | def __init__(self, **properties): 11 | # Set Form properties and Data Bindings. 12 | self.init_components(**properties) 13 | 14 | # Any code you write here will run before the form opens. 15 | -------------------------------------------------------------------------------- /client_code/Users/PreAuth/PreAuthKeyRow/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: {type: DataRowPanel} 2 | components: 3 | - type: CheckBox 4 | properties: {role: null, align: left, tooltip: '', border: '', enabled: true, foreground: '', 5 | allow_indeterminate: false, visible: true, text: '', font_size: null, font: '', 6 | spacing_above: small, spacing_below: small, italic: false, background: '', bold: false, 7 | checked: false, underline: false} 8 | name: check_box_key 9 | layout_properties: {column: FZRFDK} 10 | is_package: true 11 | -------------------------------------------------------------------------------- /client_code/Users/PreAuth/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import PreAuthTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from .GenerateKey import GenerateKey 9 | import json 10 | import datetime 11 | from ... import Startup 12 | 13 | class PreAuth(PreAuthTemplate): 14 | def __init__(self, user_name, **properties): 15 | # Set Form properties and Data Bindings. 16 | self.item['selected_user'] = None 17 | self.item['is_reusable'] = False 18 | self.item['is_ephemeral'] = False 19 | self.item['expiration_date'] = '7' 20 | self.user_name = user_name 21 | self.url = Startup.url 22 | self.api_key = Startup.api_key 23 | self.response = anvil.server.call('get_preauth_keys', self.url, self.api_key, user_name) 24 | self.repeating_panel_preauth_keys.items = self.response['preAuthKeys'] 25 | self.init_components(**properties) 26 | 27 | # Any code you write here will run before the form opens. 28 | 29 | def get_selected_rows(self): 30 | selected = [] 31 | for row_template in self.repeating_panel_preauth_keys.get_components(): 32 | if row_template.check_box_key.checked: 33 | selected.append(row_template.item) 34 | return selected 35 | 36 | def button_generate_key_click(self, **event_args): 37 | """This method is called when the button is clicked""" 38 | generate_decision = alert(GenerateKey(item=self.item), title='Select a User to generate a new PreAuth key', buttons=[('Ok', True),('Cancel', False)]) 39 | if generate_decision: 40 | # get the current date and time in UTC format 41 | now = datetime.datetime.utcnow() 42 | # add the input number of days to the current date and time 43 | future_date = now + datetime.timedelta(days=int(self.item['expiration_date'])) 44 | # convert the future date to ISO 8601 format with UTC timezone 45 | iso_date = future_date.replace(microsecond=0).isoformat() + 'Z' 46 | key_dict = {'user': self.item['selected_user'], 'reusable': self.item['is_reusable'], 'ephemeral': self.item['is_ephemeral'], 'expiration': iso_date} 47 | json_string = json.dumps(key_dict) 48 | return_response = anvil.server.call('add_preauth_key', self.url, self.api_key, json_string) 49 | if return_response['status'] == 'True': 50 | notification_body = return_response['body']['preAuthKey']['key'] + ' generated for user ' + self.user_name + ' and will expire on ' + return_response['body']['preAuthKey']['expiration'] 51 | notification_title = 'PreAuth Key Generated Successfully' 52 | else: 53 | notification_body = 'Unable to Generate a PreAuth key for user ' + self.user_name 54 | notification_title = 'Error Occurred' 55 | Notification(notification_body, timeout=6, title=notification_title).show() 56 | self.response = anvil.server.call('get_preauth_keys', self.url, self.api_key, self.user_name) 57 | self.repeating_panel_preauth_keys.items = self.response['preAuthKeys'] 58 | 59 | def button_1_click(self, **event_args): 60 | """This method is called when the button is clicked""" 61 | selected = self.get_selected_rows() 62 | for key_rows in selected: 63 | keys = key_rows.get('key') 64 | users = key_rows.get('user') 65 | json_string = '{"user": "'+users+'", "key": "'+keys+'"}' 66 | anvil.server.call('expire_preauth_key', self.url, self.api_key, json_string) 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /client_code/Users/PreAuth/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {col_widths: '{}'} 4 | components: 5 | - type: form:Home.Header 6 | properties: {} 7 | name: header_1 8 | layout_properties: {grid_position: 'WCMTGB,JYNGSM', full_width_row: true} 9 | - type: GridPanel 10 | properties: {spacing_above: small, spacing_below: small, background: '', foreground: '', 11 | border: '', visible: true, role: null, tooltip: ''} 12 | name: grid_panel_container 13 | layout_properties: {grid_position: 'MDEBHZ,ZPUWEO', full_width_row: true} 14 | components: 15 | - type: DataGrid 16 | properties: 17 | role: null 18 | columns: 19 | - {id: PJIVNW, title: Key, data_key: key, $$hashKey: 'object:60009', width: '225', 20 | expand: true} 21 | - {id: WWADRM, title: Expiration, data_key: expiration, $$hashKey: 'object:60010', 22 | expand: true, width: ''} 23 | - {id: OLEACZ, title: Created At, data_key: createdAt, $$hashKey: 'object:60044', 24 | expand: true} 25 | - {id: YKFNKO, title: Ephemeral, data_key: ephemeral, $$hashKey: 'object:60008', 26 | width: '90', expand: false} 27 | - {id: EKKGHH, title: Reusable, data_key: reusable, $$hashKey: 'object:60046', 28 | width: '80'} 29 | - {id: KQKAWX, title: Used, data_key: used, $$hashKey: 'object:60048', width: '70'} 30 | - {id: FZRFDK, title: '', data_key: '', $$hashKey: 'object:62888', width: '40'} 31 | auto_header: true 32 | tooltip: '' 33 | border: '' 34 | foreground: '' 35 | rows_per_page: 20 36 | visible: true 37 | wrap_on: never 38 | show_page_controls: true 39 | spacing_above: small 40 | spacing_below: small 41 | background: '' 42 | name: data_grid_preauth_keys 43 | layout_properties: {grid_position: 'LVOVJJ,TIMEFJ', row: LCKOZS, width_xs: 12, 44 | col_xs: 0, width: 1106} 45 | components: 46 | - type: RepeatingPanel 47 | properties: {spacing_above: none, spacing_below: none, item_template: Users.PreAuth.PreAuthKeyRow} 48 | name: repeating_panel_preauth_keys 49 | layout_properties: {} 50 | - type: GridPanel 51 | properties: {spacing_above: small, spacing_below: none, background: '', foreground: '', 52 | border: '', visible: true, role: null, tooltip: ''} 53 | name: grid_panel_1 54 | layout_properties: {slot: footer} 55 | components: 56 | - type: Button 57 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true, 58 | foreground: '', visible: true, text: Generate Preauth Key, font_size: null, 59 | font: '', spacing_above: none, icon_align: left, spacing_below: none, italic: false, 60 | background: '', bold: false, underline: false, icon: 'fa:refresh'} 61 | name: button_generate_key 62 | layout_properties: {row: YYWVFW, width_xs: 4, col_xs: 0, width: 265.194} 63 | event_bindings: {click: button_generate_key_click} 64 | - type: Button 65 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 66 | foreground: '', visible: true, text: Expire Preauth Key, font_size: null, 67 | font: '', spacing_above: none, icon_align: left, spacing_below: none, italic: false, 68 | background: '', bold: false, underline: false, icon: 'fa:times-circle'} 69 | name: button_1 70 | layout_properties: {row: YYWVFW, width_xs: 4, col_xs: 4, width: 265.194} 71 | event_bindings: {click: button_1_click} 72 | is_package: true 73 | -------------------------------------------------------------------------------- /client_code/Users/UserRow/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import UserRowTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from ..PreAuth import PreAuth 9 | from ... import Startup 10 | 11 | class UserRow(UserRowTemplate): 12 | def __init__(self, **properties): 13 | self.url = Startup.url 14 | self.api_key = Startup.api_key 15 | # Set Form properties and Data Bindings. 16 | self.init_components(**properties) 17 | 18 | # Any code you write here will run before the form opens. 19 | 20 | def button_cancel_click(self, **event_args): 21 | """This method is called when the button is clicked""" 22 | self.data_row_panel_edit.visible = False 23 | self.data_row_panel_view.visible = True 24 | 25 | def button_update_click(self, **event_args): 26 | """This method is called when the button is clicked""" 27 | pass 28 | 29 | def button_delete_user_click(self, **event_args): 30 | """This method is called when the button is clicked""" 31 | anvil.server.call('delete_user', self.url, self.api_key, self.item['name']) 32 | self.item.delete() 33 | #anvil.server.call('delete_hs_table_row', self.item) 34 | self.parent.raise_event('x-refresh') 35 | 36 | def link_id_click(self, **event_args): 37 | """This method is called when the link is clicked""" 38 | self.data_row_panel_view.visible = False 39 | self.data_row_panel_edit.visible = True 40 | 41 | def button_preauth_keys_click(self, **event_args): 42 | """This method is called when the button is clicked""" 43 | form = get_open_form() 44 | form.column_panel_home.clear() 45 | form.column_panel_home.add_component(PreAuth(self.label_name.text)) 46 | #alert(PreAuth(self.label_name.text), large=True) 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /client_code/Users/UserRow/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: DataRowPanel 3 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: false, 4 | visible: true, font_size: null, font: '', spacing_above: none, spacing_below: none, 5 | italic: false, background: '', bold: false, underline: false} 6 | components: 7 | - type: DataRowPanel 8 | properties: {role: null, align: left, tooltip: '', border: none, foreground: '', 9 | auto_display_data: true, visible: true, item: null, font_size: null, font: '', 10 | spacing_above: none, spacing_below: none, italic: false, background: '', bold: false, 11 | underline: false} 12 | name: data_row_panel_view 13 | layout_properties: {column: null} 14 | components: 15 | - type: Link 16 | properties: {role: null, url: '', align: left, tooltip: '', border: '', foreground: '', 17 | visible: true, text: '', font_size: null, wrap_on: mobile, font: '', col_spacing: medium, 18 | spacing_above: small, icon_align: left, col_widths: '{}', spacing_below: small, 19 | italic: false, background: '', bold: false, underline: false, icon: 'fa:user'} 20 | name: link_id 21 | layout_properties: {column: GQMYHE} 22 | data_bindings: 23 | - {property: text, code: 'self.item[''id'']'} 24 | components: [] 25 | event_bindings: {click: link_id_click} 26 | - type: Label 27 | properties: {} 28 | name: label_name 29 | layout_properties: {column: VHVAOD} 30 | data_bindings: 31 | - {property: text, code: 'self.item[''name'']'} 32 | data_bindings: 33 | - {property: item, code: self.item} 34 | - type: DataRowPanel 35 | properties: {role: null, align: left, tooltip: '', border: '', foreground: '', auto_display_data: true, 36 | visible: false, item: null, font_size: null, font: '', spacing_above: none, spacing_below: none, 37 | italic: false, background: '', bold: false, underline: false} 38 | name: data_row_panel_edit 39 | layout_properties: {column: null} 40 | data_bindings: 41 | - {property: item, code: self.item} 42 | components: 43 | - type: TextBox 44 | properties: {} 45 | name: text_box_username 46 | layout_properties: {column: VHVAOD} 47 | data_bindings: 48 | - {property: text, code: 'self.item[''name'']', writeback: false} 49 | - type: GridPanel 50 | properties: {spacing_above: none, spacing_below: none, background: '', foreground: '', 51 | border: '', visible: true, role: null, tooltip: ''} 52 | name: grid_panel_1 53 | layout_properties: {column: null} 54 | components: 55 | - type: ColumnPanel 56 | properties: {col_widths: '{}'} 57 | name: column_panel_1 58 | layout_properties: {row: JGAIMY, width_xs: 5, col_xs: 0} 59 | components: 60 | - type: Button 61 | properties: {role: filled, align: center, tooltip: '', border: '', enabled: true, 62 | foreground: '', visible: true, text: Delete User, font_size: null, font: '', 63 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 64 | background: '', bold: false, underline: false, icon: 'fa:trash'} 65 | name: button_delete_user 66 | layout_properties: {row: JGAIMY, width_xs: 3, col_xs: 0, width: 158.5, grid_position: 'BICXUX,RLVHZW'} 67 | event_bindings: {click: button_delete_user_click} 68 | - type: Button 69 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 70 | foreground: '', visible: true, text: Pre-Auth Keys, font_size: null, font: '', 71 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 72 | background: '', bold: false, underline: false, icon: 'fa:key'} 73 | name: button_preauth_keys 74 | layout_properties: {row: JGAIMY, width_xs: 3, col_xs: 3, width: 158.5, grid_position: 'BICXUX,JHSEFB'} 75 | event_bindings: {click: button_preauth_keys_click} 76 | - type: Button 77 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true, 78 | foreground: '', visible: true, text: Cancel, font_size: null, font: '', spacing_above: small, 79 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 80 | underline: false, icon: 'fa:times-circle'} 81 | name: button_cancel 82 | layout_properties: {grid_position: 'GWPDGT,YBCQHC', row: JGAIMY, width_xs: 2, 83 | col_xs: 8, width: 132.328125} 84 | event_bindings: {click: button_cancel_click} 85 | - type: Button 86 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 87 | foreground: '', visible: true, text: Update, font_size: null, font: '', spacing_above: small, 88 | icon_align: left, spacing_below: small, italic: false, background: '', bold: false, 89 | underline: false, icon: 'fa:floppy-o'} 90 | name: button_update 91 | layout_properties: {grid_position: 'GWPDGT,UWURVQ', row: JGAIMY, width_xs: 2, 92 | col_xs: 10, width: 132.328125} 93 | event_bindings: {click: button_update_click} 94 | is_package: true 95 | -------------------------------------------------------------------------------- /client_code/Users/__init__.py: -------------------------------------------------------------------------------- 1 | from ._anvil_designer import UsersTemplate 2 | from anvil import * 3 | import anvil.server 4 | import anvil.users 5 | import anvil.tables as tables 6 | import anvil.tables.query as q 7 | from anvil.tables import app_tables 8 | from .. import Startup 9 | class Users(UsersTemplate): 10 | def __init__(self, **properties): 11 | self.url = Startup.url 12 | self.api_key = Startup.api_key 13 | self.item = anvil.server.call('get_hs_users_table').search() 14 | self.init_components(**properties) 15 | self.repeating_panel_users.set_event_handler('x-refresh', self.refresh_data) 16 | #self.repeating_panel_users.items = self.item.search() 17 | # Any code you write here will run before the form opens. 18 | 19 | def refresh_data(self, **event_args): 20 | self.item = anvil.server.call('get_hs_users_table').search() 21 | 22 | def button_save_new_user_click(self, **event_args): 23 | """This method is called when the button is clicked""" 24 | user_name = self.text_box_username.text 25 | json_string = '{"name": "'+user_name+'"}' 26 | status = anvil.server.call('add_user', self.url, self.api_key, json_string) 27 | if status == True: 28 | Notification(f'Username {self.text_box_username.text} added succesfully').show() 29 | anvil.server.call('record_users') 30 | self.item = anvil.server.call('get_hs_users_table').search() 31 | self.column_panel_new_user.visible = False 32 | self.button_add_user.visible = True 33 | else: 34 | Notification('Unable to add user to Headscale').show() 35 | self.column_panel_new_user.visible = False 36 | self.button_add_user.visible = True 37 | 38 | def button_add_user_click(self, **event_args): 39 | """This method is called when the button is clicked""" 40 | self.button_add_user.visible = False 41 | self.column_panel_new_user.visible = True 42 | 43 | def button_cancel_click(self, **event_args): 44 | """This method is called when the button is clicked""" 45 | self.column_panel_new_user.visible = False 46 | self.button_add_user.visible = True 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /client_code/Users/form_template.yaml: -------------------------------------------------------------------------------- 1 | container: 2 | type: ColumnPanel 3 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: true, 4 | wrap_on: mobile, col_spacing: tiny, spacing_above: small, col_widths: '{}', spacing_below: small, 5 | background: ''} 6 | data_bindings: [] 7 | components: 8 | - type: form:Home.Header 9 | properties: {} 10 | name: header_1 11 | layout_properties: {grid_position: 'ZGVNOS,YOROMF', full_width_row: true} 12 | - type: Label 13 | properties: {role: title, align: center, tooltip: '', border: '', foreground: '', 14 | visible: true, text: Headscale Users, font_size: null, font: '', spacing_above: none, 15 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false, 16 | underline: true, icon: 'fa:users'} 17 | name: label_title 18 | layout_properties: {grid_position: 'KJKRCR,AFPXXY'} 19 | - type: DataGrid 20 | properties: 21 | role: null 22 | columns: 23 | - {id: GQMYHE, title: Id, data_key: id, $$hashKey: 'object:11412', width: '60'} 24 | - {id: VHVAOD, title: Name, data_key: name, $$hashKey: 'object:11413'} 25 | - {id: JFSQGB, title: CreatedAt, data_key: createdAt, $$hashKey: 'object:11414'} 26 | auto_header: true 27 | tooltip: '' 28 | border: '' 29 | foreground: '' 30 | rows_per_page: 20 31 | visible: true 32 | wrap_on: never 33 | show_page_controls: true 34 | spacing_above: small 35 | spacing_below: small 36 | background: '' 37 | name: data_grid_1 38 | layout_properties: {grid_position: 'AMHIAO,EPUTYC'} 39 | components: 40 | - type: RepeatingPanel 41 | properties: {spacing_above: none, spacing_below: none, item_template: Users.UserRow} 42 | name: repeating_panel_users 43 | layout_properties: {} 44 | data_bindings: 45 | - {property: items, code: self.item} 46 | - type: GridPanel 47 | properties: {spacing_above: small, spacing_below: none, background: '', foreground: '', 48 | border: '', visible: true, role: null, tooltip: ''} 49 | name: grid_panel_footer 50 | layout_properties: {slot: footer, grid_position: 'KNMQKZ,ZTRYOJ'} 51 | components: 52 | - type: Button 53 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 54 | foreground: '', visible: true, text: Add User, font_size: null, font: '', 55 | spacing_above: small, icon_align: left, spacing_below: none, italic: false, 56 | background: '', bold: false, underline: false, icon: 'fa:user'} 57 | name: button_add_user 58 | layout_properties: {row: LJSSCW, width_xs: 3, col_xs: 0, width: 184.34375} 59 | event_bindings: {click: button_add_user_click} 60 | - type: ColumnPanel 61 | properties: {role: null, tooltip: '', border: '', foreground: '', visible: false, 62 | wrap_on: mobile, col_spacing: medium, spacing_above: none, col_widths: '{"TAWCJI":10,"VYGRLX":35,"WCRMFS":13,"ITNSKN":15}', 63 | spacing_below: none, background: ''} 64 | name: column_panel_new_user 65 | layout_properties: {column: VHVAOD, slot: footer, row: LJSSCW, width_xs: 9, 66 | col_xs: 0, width: 613.046875} 67 | components: 68 | - type: Label 69 | properties: {role: null, align: center, tooltip: '', border: '', foreground: '', 70 | visible: true, text: 'Username:', font_size: null, font: '', spacing_above: small, 71 | icon_align: left, spacing_below: small, italic: false, background: '', bold: true, 72 | underline: true, icon: ''} 73 | name: label_username 74 | layout_properties: {grid_position: 'YXLDXX,TAWCJI'} 75 | - type: TextBox 76 | properties: {role: null, align: left, hide_text: false, tooltip: '', placeholder: Type a new Username, 77 | border: '', enabled: true, foreground: '', visible: true, text: '', font_size: null, 78 | font: '', spacing_above: small, type: text, spacing_below: small, italic: false, 79 | background: '', bold: false, underline: false} 80 | name: text_box_username 81 | layout_properties: {grid_position: 'YXLDXX,VYGRLX'} 82 | - type: Button 83 | properties: {role: filled, align: right, tooltip: '', border: '', enabled: true, 84 | foreground: '', visible: true, text: Cancel, font_size: null, font: '', 85 | spacing_above: small, icon_align: left, spacing_below: small, italic: false, 86 | background: '', bold: false, underline: false, icon: 'fa:times-circle'} 87 | name: button_cancel 88 | layout_properties: {grid_position: 'YXLDXX,ITNSKN'} 89 | event_bindings: {click: button_cancel_click} 90 | - type: Button 91 | properties: {role: filled, align: left, tooltip: '', border: '', enabled: true, 92 | foreground: '', visible: true, text: Save, font_size: null, font: '', spacing_above: small, 93 | icon_align: left, spacing_below: none, italic: false, background: '', bold: false, 94 | underline: false, icon: 'fa:floppy-o'} 95 | name: button_save_new_user 96 | layout_properties: {row: MVIIHF, width_xs: 5, col_xs: 0, width: 122.484375, 97 | grid_position: 'YXLDXX,WCRMFS'} 98 | event_bindings: {click: button_save_new_user_click} 99 | is_package: true 100 | -------------------------------------------------------------------------------- /server_code/API_Interface.py: -------------------------------------------------------------------------------- 1 | import anvil.tables as tables 2 | from anvil.tables import app_tables 3 | import anvil.server 4 | import requests 5 | from datetime import timedelta, date, datetime 6 | from dateutil import parser 7 | 8 | ################################################################## 9 | # Functions related to HEADSCALE and API KEYS 10 | ################################################################## 11 | # Tests the API key 12 | @anvil.server.callable 13 | def test_api_key(url, api_key): 14 | response = requests.get( 15 | str(url)+"/api/v1/apikey", 16 | headers={ 17 | 'Accept': 'application/json', 18 | 'Authorization': 'Bearer '+str(api_key) 19 | } 20 | ) 21 | return response.status_code 22 | 23 | # Expires an API key 24 | @anvil.server.callable 25 | def expire_key(url, api_key): 26 | payload = {'prefix':str(api_key[0:10])} 27 | json_payload=json.dumps(payload) 28 | print("Sending the payload '"+str(json_payload)+"' to the headscale server") 29 | 30 | response = requests.post( 31 | str(url)+"/api/v1/apikey/expire", 32 | data=json_payload, 33 | headers={ 34 | 'Accept': 'application/json', 35 | 'Content-Type': 'application/json', 36 | 'Authorization': 'Bearer '+str(api_key) 37 | } 38 | ) 39 | return response.status_code 40 | 41 | # Checks if the key needs to be renewed 42 | # If it does, renews the key, then expires the old key 43 | @anvil.server.callable 44 | def renew_api_key(url, api_key): 45 | # 0 = Key has been updated or key is not in need of an update 46 | # 1 = Key has failed validity check or has failed to write the API key 47 | # Check when the key expires and compare it to todays date: 48 | key_info = get_api_key_info(url, api_key) 49 | expiration_time = key_info["expiration"] 50 | today_date = date.today() 51 | expire = parser.parse(expiration_time) 52 | expire_fmt = str(expire.year) + "-" + str(expire.month).zfill(2) + "-" + str(expire.day).zfill(2) 53 | expire_date = date.fromisoformat(expire_fmt) 54 | delta = expire_date - today_date 55 | tmp = today_date + timedelta(days=90) 56 | new_expiration_date = str(tmp)+"T00:00:00.000000Z" 57 | 58 | # If the delta is less than 5 days, renew the key: 59 | if delta < timedelta(days=5): 60 | print("Key is about to expire. Delta is "+str(delta)) 61 | payload = {'expiration':str(new_expiration_date)} 62 | json_payload=json.dumps(payload) 63 | print("Sending the payload '"+str(json_payload)+"' to the headscale server") 64 | 65 | response = requests.post( 66 | str(url)+"/api/v1/apikey", 67 | data=json_payload, 68 | headers={ 69 | 'Accept': 'application/json', 70 | 'Content-Type': 'application/json', 71 | 'Authorization': 'Bearer '+str(api_key) 72 | } 73 | ) 74 | new_key = response.json() 75 | print("JSON: "+json.dumps(new_key)) 76 | print("New Key is: "+new_key["apiKey"]) 77 | api_key_test = test_api_key(url, new_key["apiKey"]) 78 | print("Testing the key: "+str(api_key_test)) 79 | # Test if the new key works: 80 | if api_key_test == 200: 81 | print("The new key is valid and we are writing it to the file") 82 | if not set_api_key(new_key["apiKey"]): 83 | print("We failed writing the new key!") 84 | return False, 'Key write failed', '' 85 | print("Key validated and written. Moving to expire the key.") 86 | expire_key(url, api_key) 87 | return True, 'Key updated and validated', new_key['apiKey'] 88 | else: 89 | print("Testing the API key failed.") 90 | return False, 'The API Key test failed', '' 91 | else: return True, 'No Update Required', '' # No work is required 92 | 93 | # Gets information about the current API key 94 | @anvil.server.callable 95 | def get_api_key_info(url, api_key): 96 | print("Getting API key information") 97 | response = requests.get( 98 | str(url)+"/api/v1/apikey", 99 | headers={ 100 | 'Accept': 'application/json', 101 | 'Authorization': 'Bearer '+str(api_key) 102 | } 103 | ) 104 | json_response = response.json() 105 | # Find the current key in the array: 106 | key_prefix = str(api_key[0:10]) 107 | print("Looking for valid API Key...") 108 | for key in json_response["apiKeys"]: 109 | if key_prefix == key["prefix"]: 110 | print("Key found.") 111 | return key 112 | print("Could not find a valid key in Headscale. Need a new API key.") 113 | return "Key not found" 114 | 115 | ################################################################## 116 | # Functions related to MACHINES 117 | ################################################################## 118 | 119 | # register a new machine 120 | @anvil.server.callable 121 | def register_machine(url, api_key, machine_key, user): 122 | print("Registering machine %s to user %s", str(machine_key), str(user)) 123 | response = requests.post( 124 | str(url)+"/api/v1/node/register?user="+str(user)+"&key="+str(machine_key), 125 | headers={ 126 | 'Accept': 'application/json', 127 | 'Authorization': 'Bearer '+str(api_key) 128 | } 129 | ) 130 | return response.json() 131 | 132 | 133 | # Sets the machines tags 134 | @anvil.server.callable 135 | def set_machine_tags(url, api_key, machine_id, tags_list): 136 | print("Setting machine_id %s tag %s", str(machine_id), str(tags_list)) 137 | response = requests.post( 138 | str(url)+"/api/v1/node/"+str(machine_id)+"/tags", 139 | data=tags_list, 140 | headers={ 141 | 'Accept': 'application/json', 142 | 'Content-Type': 'application/json', 143 | 'Authorization': 'Bearer '+str(api_key) 144 | } 145 | ) 146 | return response.json() 147 | 148 | # Moves machine_id to user "new_user" 149 | @anvil.server.callable 150 | def move_user(url, api_key, machine_id, new_user): 151 | print("Moving machine_id %s to user %s", str(machine_id), str(new_user)) 152 | response = requests.post( 153 | str(url)+"/api/v1/node/"+str(machine_id)+"/user?user="+str(new_user), 154 | headers={ 155 | 'Accept': 'application/json', 156 | 'Authorization': 'Bearer '+str(api_key) 157 | } 158 | ) 159 | return response.json() 160 | @anvil.server.callable 161 | def update_route(url, api_key, route_id, current_state): 162 | if current_state == False: 163 | action = "disable" 164 | elif current_state == True: 165 | action = "enable" 166 | print(f"Updating Route {route_id}: Action: {action}") 167 | 168 | # Debug 169 | print("URL: "+str(url)) 170 | print("Route ID: "+str(route_id)) 171 | print("Current State: "+str(current_state)) 172 | print("Action to take: "+str(action)) 173 | 174 | response = requests.post( 175 | str(url)+"/api/v1/routes/"+str(route_id)+"/"+str(action), 176 | headers={ 177 | 'Accept': 'application/json', 178 | 'Authorization': 'Bearer '+str(api_key) 179 | } 180 | ) 181 | return response.json() 182 | 183 | @anvil.server.callable 184 | # Get all machines on the Headscale network 185 | def get_machines(url, api_key): 186 | response = requests.get( 187 | str(url)+"/api/v1/node", 188 | headers={ 189 | 'Accept': 'application/json', 190 | 'Authorization': 'Bearer '+str(api_key) 191 | } 192 | ) 193 | return response.json() 194 | 195 | @anvil.server.callable 196 | # Get machine with "machine_id" on the Headscale network 197 | def get_machine_info(url, api_key, machine_id): 198 | print("Getting information for machine ID %s", str(machine_id)) 199 | response = requests.get( 200 | str(url)+"/api/v1/node/"+str(machine_id), 201 | headers={ 202 | 'Accept': 'application/json', 203 | 'Authorization': 'Bearer '+str(api_key) 204 | } 205 | ) 206 | return response.json() 207 | 208 | @anvil.server.callable 209 | # Delete a machine from Headscale 210 | def delete_machine(url, api_key, machine_id): 211 | print("Deleting machine %s", str(machine_id)) 212 | response = requests.delete( 213 | str(url)+"/api/v1/node/"+str(machine_id), 214 | headers={ 215 | 'Accept': 'application/json', 216 | 'Authorization': 'Bearer '+str(api_key) 217 | } 218 | ) 219 | status = "True" if response.status_code == 200 else "False" 220 | if response.status_code == 200: 221 | print("Machine deleted.") 222 | else: 223 | print("Deleting machine failed! %s", str(response.json())) 224 | return {"status": status, "body": response.json()} 225 | 226 | @anvil.server.callable 227 | # Rename "machine_id" with name "new_name" 228 | def rename_machine(url, api_key, machine_id, new_name): 229 | print("Renaming machine %s", str(machine_id)) 230 | response = requests.post( 231 | str(url)+"/api/v1/node/"+str(machine_id)+"/rename/"+str(new_name), 232 | headers={ 233 | 'Accept': 'application/json', 234 | 'Authorization': 'Bearer '+str(api_key) 235 | } 236 | ) 237 | status = "True" if response.status_code == 200 else "False" 238 | if response.status_code == 200: 239 | print("Machine renamed") 240 | else: 241 | print("Machine rename failed! %s", str(response.json())) 242 | return {"status": status, "body": response.json()} 243 | 244 | @anvil.server.callable 245 | # Gets routes for the passed machine_id 246 | def get_machine_routes(url, api_key, machine_id): 247 | print("Getting routes for machine %s", str(machine_id)) 248 | response = requests.get( 249 | str(url)+"/api/v1/node/"+str(machine_id)+"/routes", 250 | headers={ 251 | 'Accept': 'application/json', 252 | 'Authorization': 'Bearer '+str(api_key) 253 | } 254 | ) 255 | if response.status_code == 200: 256 | print("Routes obtained") 257 | else: 258 | print("Failed to get routes: %s", str(response.json())) 259 | return response.json() 260 | 261 | @anvil.server.callable 262 | # Gets routes for the entire tailnet 263 | def get_routes(url, api_key): 264 | print("Getting routes") 265 | response = requests.get( 266 | str(url)+"/api/v1/routes", 267 | headers={ 268 | 'Accept': 'application/json', 269 | 'Authorization': 'Bearer '+str(api_key) 270 | } 271 | ) 272 | return response.json() 273 | ################################################################## 274 | # Functions related to USERS 275 | ################################################################## 276 | 277 | @anvil.server.callable 278 | # Get all users in use 279 | def get_users(url, api_key): 280 | print("Getting Users") 281 | response = requests.get( 282 | str(url)+"/api/v1/user", 283 | headers={ 284 | 'Accept': 'application/json', 285 | 'Authorization': 'Bearer '+str(api_key) 286 | } 287 | ) 288 | return response.json() 289 | 290 | @anvil.server.callable 291 | # Rename "old_name" with name "new_name" 292 | def rename_user(url, api_key, old_name, new_name): 293 | print("Renaming user %s to %s.", str(old_name), str(new_name)) 294 | response = requests.post( 295 | str(url)+"/api/v1/user/"+str(old_name)+"/rename/"+str(new_name), 296 | headers={ 297 | 'Accept': 'application/json', 298 | 'Authorization': 'Bearer '+str(api_key) 299 | } 300 | ) 301 | status = "True" if response.status_code == 200 else "False" 302 | if response.status_code == 200: 303 | print("User renamed.") 304 | else: 305 | print("Renaming User failed!") 306 | return {"status": status, "body": response.json()} 307 | 308 | @anvil.server.callable 309 | # Delete a user from Headscale 310 | def delete_user(url, api_key, user_name): 311 | print("Deleting a User: %s", str(user_name)) 312 | response = requests.delete( 313 | str(url)+"/api/v1/user/"+str(user_name), 314 | headers={ 315 | 'Accept': 'application/json', 316 | 'Authorization': 'Bearer '+str(api_key) 317 | } 318 | ) 319 | status = "True" if response.status_code == 200 else "False" 320 | if response.status_code == 200: 321 | print("User deleted.") 322 | else: 323 | print("Deleting User failed!") 324 | return {"status": status, "body": response.json()} 325 | 326 | @anvil.server.callable 327 | # Add a user from Headscale 328 | def add_user(url, api_key, data): 329 | print("Adding user: %s", str(data)) 330 | response = requests.post( 331 | str(url)+"/api/v1/user", 332 | data=data, 333 | headers={ 334 | 'Accept': 'application/json', 335 | 'Content-Type': 'application/json', 336 | 'Authorization': 'Bearer '+str(api_key) 337 | } 338 | ) 339 | status = True if response.status_code == 200 else False 340 | if response.status_code == 200: 341 | print("User added.") 342 | else: 343 | print("Adding User failed!") 344 | return status 345 | 346 | ################################################################## 347 | # Functions related to PREAUTH KEYS in USERS 348 | ################################################################## 349 | 350 | @anvil.server.callable 351 | # Get all PreAuth keys associated with a user "user_name" 352 | def get_preauth_keys(url, api_key, user_name): 353 | print("Getting PreAuth Keys in User %s", str(user_name)) 354 | response = requests.get( 355 | str(url)+"/api/v1/preauthkey?user="+str(user_name), 356 | headers={ 357 | 'Accept': 'application/json', 358 | 'Authorization': 'Bearer '+str(api_key) 359 | } 360 | ) 361 | return response.json() 362 | 363 | @anvil.server.callable 364 | # Add a preauth key to the user "user_name" given the booleans "ephemeral" 365 | # and "reusable" with the expiration date "date" contained in the JSON payload "data" 366 | def add_preauth_key(url, api_key, data): 367 | print("Adding PreAuth Key: %s", str(data)) 368 | response = requests.post( 369 | str(url)+"/api/v1/preauthkey", 370 | data=data, 371 | headers={ 372 | 'Accept': 'application/json', 373 | 'Content-Type': 'application/json', 374 | 'Authorization': 'Bearer '+str(api_key) 375 | } 376 | ) 377 | status = "True" if response.status_code == 200 else "False" 378 | if response.status_code == 200: 379 | print("PreAuth Key added.") 380 | else: 381 | print("Adding PreAuth Key failed!") 382 | print(response) 383 | return {"status": status, "body": response.json()} 384 | 385 | @anvil.server.callable 386 | # Expire a pre-auth key. data is {"user": "string", "key": "string"} 387 | def expire_preauth_key(url, api_key, data): 388 | print("Expiring PreAuth Key...") 389 | response = requests.post( 390 | str(url)+"/api/v1/preauthkey/expire", 391 | data=data, 392 | headers={ 393 | 'Accept': 'application/json', 394 | 'Content-Type': 'application/json', 395 | 'Authorization': 'Bearer '+str(api_key) 396 | } 397 | ) 398 | status = "True" if response.status_code == 200 else "False" 399 | print("expire_preauth_key - Return: "+str(response.json())) 400 | print("expire_preauth_key - Status: "+str(status)) 401 | return {"status": status, "body": response.json()} 402 | -------------------------------------------------------------------------------- /server_code/Hostess.py: -------------------------------------------------------------------------------- 1 | import anvil.tables as tables 2 | from anvil.tables import app_tables 3 | import anvil.server 4 | 5 | @anvil.server.callable 6 | def get_machine_table(): 7 | return app_tables.machines.client_writable_cascade() 8 | 9 | @anvil.server.callable 10 | def get_hs_users_table(): 11 | return app_tables.hs_users.client_writable_cascade() 12 | 13 | @anvil.server.callable 14 | def get_routes_tables(): 15 | return app_tables.routes.client_writable_cascade() 16 | 17 | @anvil.server.callable 18 | @tables.in_transaction 19 | def check_fresh_install(): 20 | if len(app_tables.users.search()) == 0: 21 | app_tables.users.add_row(confirmed_email=True, email='admin@milliner.login', enabled=True, password_hash='$2y$10$QoAnY4j687bSu/DI/C4n7.Wm2kLIT8fIyCid8406DvqYuBJayaFUK') 22 | created_user = True 23 | else: 24 | created_user = False 25 | if len(app_tables.settings.search()) == 0: 26 | settings_exists = False 27 | else: 28 | settings_exists = True 29 | return {'created_user': created_user, 'settings_exists': settings_exists} 30 | 31 | @anvil.server.callable 32 | def get_app_users_table(): 33 | return app_tables.users.client_writable_cascade() 34 | 35 | @anvil.server.callable 36 | def update_app_user(email, item, pw_update, password): 37 | appuser_row = app_tables.users.get_by_id(item.get_id()) 38 | if pw_update == False: 39 | appuser_row.update(email=email) 40 | elif pw_update == True: 41 | import bcrypt 42 | password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) 43 | appuser_row.update(email=email, password_hash=password_hash.decode('utf-8')) 44 | 45 | @anvil.server.callable 46 | def add_appuser(email, enabled, confirmed_email, password): 47 | import bcrypt 48 | password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) 49 | app_tables.users.add_row(email=email, enabled=enabled, confirmed_email=confirmed_email, password_hash=password_hash.decode('utf-8')) 50 | 51 | @anvil.server.callable 52 | def delete_hs_table_row(item): 53 | item.delete() 54 | 55 | @anvil.server.callable 56 | def delete_machine_row(machine_id): 57 | machine_row = app_tables.machines.get(id=machine_id) 58 | machine_row.delete() 59 | 60 | @anvil.server.callable 61 | def update_api_key_settings(new_api_key): 62 | app_tables.settings.get().update(api_key=new_api_key) 63 | 64 | -------------------------------------------------------------------------------- /server_code/RecordRecorder.py: -------------------------------------------------------------------------------- 1 | import anvil.tables as tables 2 | from anvil.tables import app_tables 3 | import anvil.server 4 | import json 5 | 6 | @anvil.server.callable 7 | @anvil.server.background_task 8 | def record_machines(): 9 | fresh_install = anvil.server.call('check_fresh_install') 10 | if not fresh_install['settings_exists']: 11 | url = '' 12 | api_key = '' 13 | elif fresh_install['settings_exists']: 14 | url = [r['url'] for r in app_tables.settings.search()][0] 15 | api_key = [r['api_key'] for r in app_tables.settings.search()][0] 16 | import datetime 17 | now = datetime.datetime.now() 18 | date_string = now.strftime("%Y-%m-%d") 19 | time_string =now.strftime("%H:%M:%S") 20 | data=anvil.server.call('get_machines', url, api_key) 21 | for machine in data['nodes']: 22 | machine_row = app_tables.machines.get(id=machine['id']) 23 | if machine_row: 24 | machine_row.update(name=machine['name'], id=machine['id'], ipAddr=machine['ipAddresses'], 25 | online=machine['online'], 26 | user= machine['user']['name'], 27 | lastSeen= machine['lastSeen'], 28 | expiry= machine['expiry'], 29 | registerMethod= machine['registerMethod'], 30 | discoKey= machine['discoKey'], 31 | createdAt= machine['createdAt'], 32 | invalidTags= machine['invalidTags'], 33 | validTags= machine['validTags'], 34 | machineKey= machine['machineKey'], 35 | lastSuccessfulUpdate= machine['lastSuccessfulUpdate'], 36 | givenName= machine['givenName'], 37 | forcedTags = machine['forcedTags'], 38 | nodeKey = machine['nodeKey'], 39 | preAuthKey= machine['preAuthKey']) 40 | else: 41 | app_tables.machines.add_row(name=machine['name'], id=machine['id'], ipAddr=machine['ipAddresses'], 42 | online=machine['online'], 43 | user= machine['user']['name'], 44 | lastSeen= machine['lastSeen'], 45 | expiry= machine['expiry'], 46 | registerMethod= machine['registerMethod'], 47 | discoKey= machine['discoKey'], 48 | createdAt= machine['createdAt'], 49 | invalidTags= machine['invalidTags'], 50 | validTags= machine['validTags'], 51 | machineKey= machine['machineKey'], 52 | lastSuccessfulUpdate= machine['lastSuccessfulUpdate'], 53 | givenName= machine['givenName'], 54 | forcedTags = machine['forcedTags'], 55 | nodeKey = machine['nodeKey'], 56 | preAuthKey= machine['preAuthKey']) 57 | response = anvil.server.call('get_api_key_info', url, api_key) 58 | settings_row = app_tables.settings.get() 59 | settings_row.update(last_hs_sync=f'{date_string}-{time_string}', api_key_creation=response['createdAt'], api_key_expiration=response['expiration']) 60 | 61 | @anvil.server.callable 62 | @anvil.server.background_task 63 | def record_users(): 64 | fresh_install = anvil.server.call('check_fresh_install') 65 | if not fresh_install['settings_exists']: 66 | url = '' 67 | api_key = '' 68 | elif fresh_install['settings_exists']: 69 | url = [r['url'] for r in app_tables.settings.search()][0] 70 | api_key = [r['api_key'] for r in app_tables.settings.search()][0] 71 | data = anvil.server.call('get_users', url, api_key) 72 | for user in data['users']: 73 | user_row = app_tables.hs_users.get(id=user['id']) 74 | if user_row: 75 | user_row.update(id=user['id'], name=user['name'], createdAt=user['createdAt']) 76 | else: 77 | app_tables.hs_users.add_row(id=user['id'], name=user['name'], createdAt=user['createdAt']) 78 | 79 | @anvil.server.callable 80 | @anvil.server.background_task 81 | def record_routes(): 82 | fresh_install = anvil.server.call('check_fresh_install') 83 | if not fresh_install['settings_exists']: 84 | url = '' 85 | api_key = '' 86 | elif fresh_install['settings_exists']: 87 | url = [r['url'] for r in app_tables.settings.search()][0] 88 | api_key = [r['api_key'] for r in app_tables.settings.search()][0] 89 | data = anvil.server.call('get_routes', url, api_key) 90 | for route in data['routes']: 91 | route_row = app_tables.routes.get(id=int(route['id'])) 92 | if route_row: 93 | route_row.update(id=int(route['id']), machineName=route['node']['name'], givenName=route['node']['givenName'], prefix=route['prefix'], enabled=route['enabled'], machineIPs=route['node']['ipAddresses']) 94 | else: 95 | app_tables.routes.add_row(id=int(route['id']), machineName=route['node']['name'], givenName=route['node']['givenName'], prefix=route['prefix'], enabled=route['enabled'], machineIPs=route['node']['ipAddresses']) 96 | -------------------------------------------------------------------------------- /theme/assets/milliner_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonp92/Milliner/482612833ce7a26aa63f21045fea37e8184c472b/theme/assets/milliner_logo.png -------------------------------------------------------------------------------- /theme/assets/milliner_spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonp92/Milliner/482612833ce7a26aa63f21045fea37e8184c472b/theme/assets/milliner_spinner.png -------------------------------------------------------------------------------- /theme/assets/standard-page.html: -------------------------------------------------------------------------------- 1 |
2 | 23 |
24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /theme/assets/theme.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto Condensed', sans-serif; 3 | font-size: 14px; 4 | font-weight: 300; 5 | background-color: %color:Background%; 6 | color: %color:On Background%; 7 | } 8 | 9 | .plot-container.plotly .main-svg .parcoords .y-axis .axis .tick > text { 10 | text-shadow: none !important; 11 | } 12 | 13 | .plot-container.plotly .main-svg .parcoords .y-axis .axis > path.domain { 14 | stroke: #C8C8CB; 15 | } 16 | 17 | /* Text and links */ 18 | 19 | .anvil-role-display { 20 | font-size: 57px; 21 | line-height: 64px; 22 | font-weight: 300; 23 | } 24 | 25 | .anvil-role-headline { 26 | font-family: Eczar, serif; 27 | font-size: 32px; 28 | line-height: 40px; 29 | font-weight: 400; 30 | } 31 | 32 | .anvil-role-title { 33 | font-size: 22px; 34 | line-height: 28px; 35 | font-weight: 400; 36 | } 37 | 38 | .anvil-role-body { 39 | font-size: 14px; 40 | line-height: 20px; 41 | font-weight: 300; 42 | } 43 | 44 | .anvil-role-input-prompt { 45 | font-size: 16px; 46 | line-height: 1.5; 47 | } 48 | 49 | .anvil-role-body > .label-text, .anvil-role-body .link-text { 50 | padding-top: 0; 51 | padding-bottom: 0; 52 | } 53 | 54 | a { 55 | color: %color:Primary%; 56 | } 57 | 58 | a:hover:not(disabled) { 59 | color: %color:Primary Container%; 60 | } 61 | 62 | a:active:not(disabled), a:focus:not(disabled){ 63 | opacity: %color:Primary Container%; 64 | } 65 | 66 | 67 | 68 | /* Text boxes and areas */ 69 | 70 | .anvil-text-box, .anvil-text-area, .form-control { 71 | color: %color:On Surface Variant%; 72 | background-color: %color:Surface Variant%; 73 | border: none; 74 | border-radius: 0; 75 | box-shadow: inset 0 -1px 0 %color:Secondary%; 76 | } 77 | 78 | .anvil-text-box:focus, .anvil-text-area:focus, .form-control:focus { 79 | box-shadow: inset 0 -2.2px 0 %color:On Surface Variant%; 80 | border-radius: 0; 81 | } 82 | 83 | .anvil-text-box[disabled], .anvil-text-area[disabled], .form-control[disabled] { 84 | background: %color:Disabled Container%; 85 | } 86 | 87 | /* Buttons */ 88 | 89 | .btn, .btn-default, .file-loader, .file-loader > label { 90 | font-size: 14px; 91 | text-transform: uppercase; 92 | font-weight: bold; 93 | letter-spacing: 2px; 94 | 95 | border: 0; 96 | background-color: transparent; 97 | background-image: none; 98 | color: %color:On Background%; 99 | text-shadow: none; 100 | box-shadow: none; 101 | -webkit-box-shadow: none; 102 | 103 | position: relative; 104 | } 105 | 106 | .btn:hover, .btn-default:hover { 107 | color: %color:On Background%; 108 | background-color: %color:Light Overlay 1%; 109 | outline: none; 110 | background-image: none; 111 | } 112 | 113 | .btn:focus, .btn:active:focus, .btn.active:focus, .btn.focus, .btn:active.focus, .btn.active.focus { 114 | color: %color:On Background%; 115 | background-color: %color:Light Overlay 2%; 116 | outline: none; 117 | outline-offset: 0px; 118 | } 119 | 120 | .btn[disabled], .btn[disabled]:hover, .btn[disabled]:focus, .file-loader.anvil-disabled > label, .file-loader.anvil-disabled > label:hover { 121 | color: %color:On Disabled%; 122 | background-color: transparent; !important 123 | } 124 | 125 | /* Inputs */ 126 | 127 | input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { 128 | outline: 0; 129 | } 130 | 131 | .file-loader:hover { 132 | color: %color:On Background%; 133 | } 134 | 135 | .file-loader > label:hover { 136 | background: %color:Light Overlay 1%; 137 | } 138 | 139 | .checkbox input, .radio input { 140 | -webkit-appearance: none; 141 | appearance: checkbox; 142 | width: 15px; 143 | height: 15px; 144 | border-radius: 1px; 145 | background: %color:Surface Variant%; 146 | } 147 | 148 | .radio input { 149 | border-radius : 50%; 150 | } 151 | 152 | .checkbox input:hover, .radio input:hover { 153 | background: linear-gradient(0deg, %color:Light Overlay 1%, %color:Light Overlay 1%), %color:Surface Variant%; 154 | } 155 | 156 | .checkbox input[disabled], radio input[disabled] { 157 | background: %color:On Disabled%; 158 | } 159 | 160 | .checkbox input[disabled]:hover, .radio input[disabled]:hover { 161 | background: %color:On Disabled%; 162 | } 163 | 164 | input[disabled] ~ span { 165 | color: %color:On Disabled%; 166 | cursor: not-allowed; 167 | } 168 | 169 | .checkbox input:checked, .radio input:checked { 170 | background-color: %color:Primary Container%; 171 | } 172 | 173 | .checkbox input:checked:before { 174 | font-family: "FontAwesome"; 175 | content: "\f00c"; 176 | font-size: 10px; 177 | display: flex; 178 | margin: 2.6px; 179 | } 180 | 181 | .radio input:checked:before { 182 | font-family: "FontAwesome"; 183 | content: "\f111"; 184 | font-size: 8px; 185 | display: flex; 186 | margin-top: 3.6px; 187 | margin-left: 4.1px 188 | } 189 | 190 | /* Column Panel */ 191 | 192 | .anvil-panel-col { 193 | padding-bottom: 10px; 194 | margin-bottom: -10px; 195 | } 196 | 197 | /* Date Picker */ 198 | 199 | .daterangepicker { 200 | color: %color:On Background%; 201 | background-color: %color:Surface Variant%; 202 | } 203 | 204 | .daterangepicker::before, .daterangepicker::after { 205 | border-bottom: 0; 206 | } 207 | 208 | .daterangepicker .calendar-table { 209 | background-color: transparent; 210 | } 211 | 212 | .daterangepicker .calendar-table td.off { 213 | background-color: transparent; 214 | color: %color:Disabled% !important; 215 | } 216 | 217 | .daterangepicker .calendar-table td.available:hover { 218 | background: linear-gradient(0deg, %color:Light Overlay 2%, %color:Light Overlay 2%), %color:Surface%; 219 | border-radius: 50%; 220 | } 221 | 222 | .daterangepicker td.active, .daterangepicker td.start-date.end-date.available { 223 | color: %color:Surface Variant%; 224 | background-color: %color:Primary%; 225 | border-radius: 50%; 226 | } 227 | 228 | .daterangepicker td.active:hover, .daterangepicker td.start-date.end-date:hover { 229 | background: linear-gradient(0deg, %color:Dark Overlay 2%, %color:Dark Overlay 2%), %color:Primary%; 230 | } 231 | 232 | .daterangepicker select { 233 | color: %color:On Background%; 234 | background: linear-gradient(0deg, %color:Light Overlay 1%, %color:Light Overlay 1%), %color:Surface%;; 235 | border: 0; 236 | } 237 | 238 | .daterangepicker select:focus { 239 | background: linear-gradient(0deg, %color:Light Overlay 1%, %color:Light Overlay 1%), %color:Surface%; 240 | border-bottom: 1.5px solid %color:On Surface Variant%; 241 | } 242 | 243 | /* Data grids */ 244 | 245 | .anvil-data-grid { 246 | background: %color:Surface Variant%; 247 | } 248 | 249 | .anvil-data-grid>.data-grid-child-panel>div.auto-grid-header { 250 | border-bottom: 2px solid %color:Outline%; 251 | } 252 | 253 | .anvil-data-row-panel>.data-row-col { 254 | padding-left: 20px; 255 | border-bottom: 1px solid %color:Outline%; 256 | } 257 | 258 | .anvil-data-row-panel>.data-row-col:not(:last-child) { 259 | border-right: 1px solid %color:Primary Overlay 2%; 260 | } 261 | 262 | /* Modals */ 263 | 264 | .modal-content { 265 | background: linear-gradient(0deg, %color:Primary Overlay 1%, %color:Primary Overlay 1%), %color:Surface%; 266 | border-radius: 2px; 267 | } 268 | 269 | .modal-footer { 270 | border: 0; 271 | } 272 | 273 | .modal-header { 274 | padding: 24px 24px 0px; 275 | font-size: 24px; 276 | line-height: 32px; 277 | border: 0; 278 | color: %color:On Surface%; 279 | } 280 | 281 | .modal-body { 282 | padding: 10px 30px; 283 | } 284 | 285 | .alert-info { 286 | background: linear-gradient(0deg, %color:Primary Overlay 2%, %color:Primary Overlay 2%), %color:Surface%; 287 | color: %color:On Surface%; 288 | border: none; 289 | /* 16dp */ box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2); 290 | } 291 | 292 | .alert-info button.close, .modal-header button.close { 293 | color: %color:On Surface%; 294 | text-shadow: none; 295 | } 296 | 297 | /* Roles */ 298 | 299 | .anvil-role-card { 300 | background: %color:Surface Variant%; 301 | padding: 8px 12px; 302 | } 303 | 304 | .anvil-role-heading { 305 | font-size: 40px; 306 | font-family: "Eczar", serif; 307 | } 308 | 309 | .anvil-role-filled .btn, .anvil-role-filled > label { 310 | background-color: %color:Primary%; 311 | padding: 8px 16px; 312 | border-radius: 10px; 313 | color: %color:On Primary%; 314 | } 315 | 316 | .anvil-role-filled .btn:hover, .anvil-role-filled > label:hover { 317 | background: linear-gradient(0deg, %color:Light Overlay 1%, %color:Light Overlay 1%), %color:Primary%; 318 | } 319 | 320 | .anvil-role-filled .btn:active, .anvil-role-filled .btn:active:focus, .anvil-role-filled > label:active, .anvil-role-filled > label:active:focus{ 321 | color: %color:On Primary%; 322 | background: linear-gradient(0deg, %color:Light Overlay 2%, %color:Light Overlay 2%), %color:Primary%; 323 | } 324 | 325 | .anvil-role-filled .btn[disabled], .anvil-role-filled .btn[disabled]:hover, .anvil-role-filled.anvil-disabled > label { 326 | color: %color:On Disabled%; 327 | background: %color:Disabled Container%; 328 | } 329 | 330 | .anvil-role-outlined .btn, .anvil-role-outlined > label { 331 | outline: 2px solid %color:Primary%; 332 | border-radius: 10px; 333 | } 334 | 335 | .anvil-role-outlined .btn:active, .anvil-role-outlined .btn:active:focus, .anvil-role-outlined > label:active, .anvil-role-outlined > label:active:focus { 336 | outline: 2px solid %color:Primary%; 337 | border-radius: 10px; 338 | outline-offset: 0px; 339 | } 340 | 341 | .anvil-role-outlined .btn[disabled], .anvil-role-outlined .btn[disabled]:hover, .anvil-role-outlined.anvil-disabled > label { 342 | outline: 2px solid %color:On Disabled%; 343 | } 344 | 345 | .anvil-role-input-error, .anvil-role-input-error:focus, .anvil-datepicker.anvil-role-input-error input { 346 | color: %color:Error%; 347 | box-shadow: inset 0 -2.2px 0 %color:Error%; 348 | border-radius: 0; 349 | } 350 | 351 | .anvil-role-tag { 352 | color: %color:B%; 353 | border-radius: 5px; 354 | padding: 0px 5px; 355 | margin-right: 5px; 356 | } 357 | 358 | /* Other classes */ 359 | 360 | .content > .placeholder { 361 | margin: 16px; 362 | color: #888; 363 | font-size: 18px; 364 | outline: 1px dotted; 365 | padding: 16px; 366 | text-align: center; 367 | } 368 | 369 | .logo-placeholder { 370 | padding: 5px; 371 | margin: 8px 0px; 372 | width: 55px; 373 | text-align: center; 374 | color: #888; 375 | outline: 1px dotted #888; 376 | } 377 | 378 | /* Scrollbars */ 379 | 380 | .content::-webkit-scrollbar { 381 | width: 10px; 382 | } 383 | 384 | .content::-webkit-scrollbar-thumb { 385 | width: 6px; 386 | background: linear-gradient(0deg, %color:Light Overlay 2%, %color:Light Overlay 2%), %color:Surface%; 387 | border-radius: 6px; 388 | -webkit-box-shadow: inset 0 0 2px rgba(0,0,0,.2); 389 | } 390 | 391 | .content::-webkit-scrollbar-track { 392 | background: linear-gradient(0deg, %color:Light Overlay 1%, %color:Light Overlay 1%), %color:Surface%; 393 | } 394 | 395 | .left-nav::-webkit-scrollbar { 396 | width: 10px; 397 | } 398 | 399 | .left-nav::-webkit-scrollbar-thumb { 400 | width: 6px; 401 | background: linear-gradient(0deg, %color:Light Overlay 2%, %color:Light Overlay 2%), %color:Surface%; 402 | border-radius: 6px; 403 | -webkit-box-shadow: inset 0 0 2px rgba(0,0,0,.2); 404 | } 405 | 406 | .left-nav::-webkit-scrollbar-track { 407 | background: linear-gradient(0deg, %color:Light Overlay 1%, %color:Light Overlay 1%), %color:Surface%; 408 | } 409 | 410 | /* Spinner */ 411 | 412 | #loadingSpinner { 413 | -webkit-animation: tada; 414 | animation: tada; 415 | animation-duration: 1.2s; 416 | animation-iteration-count: infinite; 417 | width: 150px; 418 | height: 150px; 419 | margin-left: -25px; 420 | border-radius: 5px; 421 | box-shadow: 5px -5px 19px -3px #CCAFED, 422 | -5px 5px 19px -3px #7AD2E8;; 423 | background-color: #313438; 424 | background-image: url(https://milliner.badjholdings.com/_/theme/milliner_spinner.png) !important; 425 | } 426 | 427 | /* Put things on a 4px grid */ 428 | .col-padding.col-padding-tiny { padding: 0 2px; } 429 | .column-panel.col-padding-tiny > .anvil-panel-section > .anvil-panel-section-container > .anvil-panel-section-gutter { margin: 0 -2px; } 430 | 431 | .col-padding.col-padding-small { padding: 0 4px; } 432 | .column-panel.col-padding-small > .anvil-panel-section > .anvil-panel-section-container > .anvil-panel-section-gutter { margin: 0 -4px; } 433 | 434 | .col-padding.col-padding-medium { padding: 0 8px; } 435 | .column-panel.col-padding-medium > .anvil-panel-section > .anvil-panel-section-container > .anvil-panel-section-gutter { margin: 0 -8px; } 436 | 437 | .col-padding.col-padding-large { padding: 0 12px; } 438 | .column-panel.col-padding-large > .anvil-panel-section > .anvil-panel-section-container > .anvil-panel-section-gutter { margin: 0 -12px; } 439 | 440 | .col-padding.col-padding-huge { padding: 0 20px; } 441 | .column-panel.col-padding-huge > .anvil-panel-section > .anvil-panel-section-container > .anvil-panel-section-gutter { margin: 0 -20px; } 442 | 443 | .flow-panel.flow-spacing-tiny > .flow-panel-gutter { margin: 0 -2px; } 444 | .flow-panel.flow-spacing-tiny > .flow-panel-gutter > .flow-panel-item { 445 | margin-left: 2px; 446 | margin-right: 2px; 447 | } 448 | 449 | .flow-panel.flow-spacing-small > .flow-panel-gutter { margin: 0 -4px; } 450 | .flow-panel.flow-spacing-small > .flow-panel-gutter > .flow-panel-item { 451 | margin-left: 4px; 452 | margin-right: 4px; 453 | } 454 | 455 | .flow-panel.flow-spacing-medium > .flow-panel-gutter { margin: 0 -8px; } 456 | .flow-panel.flow-spacing-medium > .flow-panel-gutter > .flow-panel-item { 457 | margin-left: 8px; 458 | margin-right: 8px; 459 | } 460 | 461 | .flow-panel.flow-spacing-large > .flow-panel-gutter { margin: 0 -12px; } 462 | .flow-panel.flow-spacing-large > .flow-panel-gutter > .flow-panel-item { 463 | margin-left: 12px; 464 | margin-right: 12px; 465 | } 466 | 467 | .flow-panel.flow-spacing-huge > .flow-panel-gutter { margin: 0 -20px; } 468 | .flow-panel.flow-spacing-huge > .flow-panel-gutter > .flow-panel-item{ 469 | margin-left: 20px; 470 | margin-right: 20px; 471 | } 472 | 473 | /* Page structure */ 474 | .structure { 475 | display: flex; 476 | flex-direction: column; 477 | min-height: 100vh; 478 | min-height: calc(100vh - %anvil-banner-height%); 479 | height: 0; /* To make flex-grow work in IE */ 480 | } 481 | 482 | /* This breaks the designer's height measurement. IE isn't supported for the designer, so set it back. */ 483 | .designer .structure { 484 | height: initial; 485 | } 486 | 487 | .nav-holder { 488 | flex-grow: 1; 489 | overflow-y: auto; 490 | } 491 | 492 | @media print { 493 | .nav-holder { 494 | overflow-y: initial; 495 | } 496 | } 497 | 498 | /* Mobile and desktop margins for content */ 499 | .content > * > .anvil-container { 500 | padding: 8px; 501 | } 502 | 503 | @media(min-width:991px) { 504 | .content > * > .anvil-container { 505 | padding: 16px 24px; 506 | } 507 | } 508 | 509 | .content .anvil-measure-this { 510 | padding-bottom: 1px; /* Prevent margin collapse messing up embedding */ 511 | } 512 | 513 | /* Allow overflows to show drop shadows in ColumnPanels 514 | This can create unwanted scrollbars; we compensate for this at the top level with .nav-holder .content {overflow-x: hidden;} 515 | */ 516 | .anvil-container-overflow { 517 | overflow-x: visible; 518 | overflow-y: visible; 519 | } 520 | 521 | .nav-holder { 522 | display: flex; 523 | align-items: stretch; 524 | flex-direction: row; 525 | } 526 | 527 | .nav-holder .left-nav { 528 | position: relative; 529 | padding: 10px; 530 | flex-shrink: 0; 531 | min-width: 160px; 532 | max-width: 400px; 533 | } 534 | 535 | .logo { 536 | margin-bottom: 20px; 537 | min-width: 40px; 538 | } 539 | 540 | .nav-shield { 541 | display: none; 542 | } 543 | 544 | .sidebar-toggle { 545 | display: none; 546 | } 547 | 548 | .left-nav-container { 549 | margin: 10px; 550 | } 551 | /* Mobile */ 552 | 553 | @media(max-width:998px) { 554 | 555 | html:not(.designer) .sidebar-toggle { 556 | display: block; 557 | padding: 8px 0px 10px 0px; 558 | text-align: center; 559 | margin: 10px; 560 | width: 35px; 561 | height: 35px; 562 | color: %color:On Background%; 563 | background: %color:Primary%; 564 | border-radius: 50%; 565 | } 566 | 567 | html:not(.designer) .sidebar-toggle:focus { 568 | background: linear-gradient(0deg, %color:Dark Overlay 1%, %color:Dark Overlay 1%), %color:Primary%; 569 | } 570 | 571 | html:not(.designer) .nav-holder { 572 | display: block; 573 | } 574 | html:not(.designer) .nav-holder .left-nav { 575 | position: fixed; 576 | top: 0; 577 | bottom: 0; 578 | width: calc(100% - 56px); 579 | max-width: 360px; 580 | z-index: 3; 581 | border-right: none; 582 | /* 16dp */ box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2); 583 | 584 | display: none; 585 | transition: right 0.5s; 586 | } 587 | 588 | html:not(.designer) .nav-holder .left-nav.shown { 589 | display: block; 590 | } 591 | 592 | .nav-shield.shown { 593 | display: block; 594 | position: fixed; 595 | top: 0; 596 | bottom: 0; 597 | left: 0; 598 | right: 0; 599 | z-index: 2; 600 | background-color: rgba(0,0,0,0.2); 601 | } 602 | } 603 | 604 | .nav-holder .left-nav, .left-nav-placeholder { 605 | display: flex; 606 | flex-direction: column; 607 | background: linear-gradient(0deg, %color:Primary Overlay 1%, %color:Primary Overlay 1%), %color:Background%; 608 | font-size: 14px; 609 | font-weight: 500; 610 | color: %color:On Surface Variant%; 611 | overflow-x: hidden; 612 | overflow-y: auto; 613 | } 614 | 615 | .left-nav-placeholder { 616 | display: block; 617 | padding: 8px; 618 | line-height: 1; 619 | width: 58px; 620 | flex-grow: 1; 621 | } 622 | .left-nav-placeholder .prompt { 623 | display: inline-block; 624 | white-space: nowrap; 625 | transform: translate(-50%,0) rotate(-90deg) translate(-50%,0) translate(15px,16px); 626 | padding: 21px 16px; 627 | color: #888; 628 | outline: 1px dotted #888; 629 | visibility: hidden; 630 | } 631 | 632 | .anvil-highlight .left-nav-placeholder .prompt { 633 | visibility: visible; 634 | } 635 | 636 | .left-nav > .column-panel { 637 | padding: 24px 0; 638 | } 639 | .left-nav > .column-panel > .anvil-panel-section > .anvil-panel-section-container:not(.full-width-row) { 640 | margin: 0 16px; 641 | width: initial; 642 | max-width: initial; 643 | overflow-x: visible; 644 | } 645 | .left-nav > .column-panel > .anvil-panel-section:first-child > .anvil-panel-section-container.full-width-row { 646 | margin-top: -24px; 647 | } 648 | .left-nav > .column-panel > .anvil-panel-section > .anvil-panel-section-container > .anvil-panel-section-gutter > .anvil-panel-row > .anvil-panel-col { 649 | overflow-x: visible; 650 | } 651 | 652 | /* Make icons look nice */ 653 | 654 | .anvil-component-icon.left-icon { 655 | width: 0; 656 | margin-right: 20px !important; 657 | position: relative; 658 | } 659 | 660 | .anvil-component-icon.left_edge-icon, .left-nav .anvil-component-icon.right_edge-icon { 661 | left: 16px; 662 | padding-top: 2px; 663 | width: 0; 664 | } 665 | 666 | .anvil-component-icon.right_edge-icon { 667 | left: initial; 668 | right: 16px; 669 | top: 0; 670 | padding-top: 2px; 671 | } 672 | 673 | .has-text > .anvil-component-icon.left-icon { 674 | margin-right: 1.3em !important; 675 | } 676 | 677 | /* Sidebar links (and labels with edge icons) go +16px wider 678 | (Top-level columns in ColumnPanels get overflow-x visible [see above]to enable this) 679 | */ 680 | 681 | .left-nav a, .left-nav .anvil-label.left_edge-icon, .left-nav .anvil-label.right_edge-icon { 682 | color: rgba(0,0,0,0.87); 683 | margin: 0 -16px; 684 | padding: 4px 16px; 685 | } 686 | 687 | .left-nav .anvil-component.left_edge-icon { 688 | padding-left: 72px; 689 | } 690 | 691 | .left-nav .anvil-role-selected { 692 | color: %color:Primary%; 693 | background-color: %color:Light Overlay 1%; 694 | } 695 | 696 | .designer .nav-holder .left-nav { 697 | min-width: 56px; 698 | } 699 | 700 | .designer .nav-holder .left-nav > .anvil-component { 701 | min-width: 160px; 702 | } 703 | 704 | .nav-holder .left-nav > .anvil-component { 705 | margin-top: 0; 706 | margin-bottom: 0; 707 | } 708 | 709 | .nav-holder .content { 710 | flex: 1; 711 | overflow-x: hidden; 712 | } 713 | 714 | .nav-holder > .logo > .placeholder { outline: 1px dotted; padding-left: 16px; padding-right: 16px; margin: 8px 8px 0;} 715 | -------------------------------------------------------------------------------- /theme/parameters.yaml: -------------------------------------------------------------------------------- 1 | roles: 2 | - name: filled 3 | components: [Button, FileLoader] 4 | - name: outlined 5 | components: [Button, FileLoader] 6 | display_in_toolbox: false 7 | - name: input-error 8 | components: [TextBox, TextArea, DatePicker] 9 | - name: card 10 | components: [ColumnPanel] 11 | display_in_toolbox: true 12 | title: Card 13 | - name: display 14 | components: [Label, Link] 15 | - name: headline 16 | components: [Label, Link] 17 | - name: title 18 | components: [Label, Link] 19 | - name: body 20 | components: [Label, Link] 21 | - name: input-prompt 22 | components: [Label, Link] 23 | color_scheme: 24 | preset_groups: 25 | - name: Colour Scheme 26 | options: 27 | - name: Material Light 28 | colors: 29 | - {name: Primary, color: '#6750A4'} 30 | - {name: Primary Container, color: '#EADDFF'} 31 | - {name: On Primary, color: '#FFFFFF'} 32 | - {name: On Primary Container, color: '#21005E'} 33 | - {name: Secondary, color: '#625B71'} 34 | - {name: Secondary Container, color: '#E8DEF8'} 35 | - {name: On Secondary, color: '#FFFFFF'} 36 | - {name: On Secondary Container, color: '#1E192B'} 37 | - {name: Tertiary, color: '#7D5260'} 38 | - {name: Tertiary Container, color: '#FFD8E4'} 39 | - {name: On Tertiary, color: '#FFFFFF'} 40 | - {name: On Tertiary Container, color: '#370B1E'} 41 | - {name: Error, color: '#B3261E'} 42 | - {name: Background, color: '#FFFBFE'} 43 | - {name: Surface, color: '#FFFBFE'} 44 | - {name: On Background, color: '#1C1B1F'} 45 | - {name: On Surface, color: '#1C1B1F'} 46 | - {name: Surface Variant, color: '#E7E0EC'} 47 | - {name: On Surface Variant, color: '#49454E'} 48 | - {name: Outline, color: '#79747E'} 49 | - {name: On Disabled, color: 'rgba(28, 27, 31, 0.38)'} 50 | - {name: Disabled Container, color: 'rgba(28, 27, 31, 0.12)'} 51 | - {name: Light Overlay 1, color: 'rgba(255, 255, 255, 0.08)'} 52 | - {name: Light Overlay 2, color: 'rgba(255, 255, 255, 0.12)'} 53 | - {name: Dark Overlay 1, color: 'rgba(30, 25, 43, 0.08)'} 54 | - {name: Dark Overlay 2, color: 'rgba(30, 25, 43, 0.12)'} 55 | - {name: Primary Overlay 1, color: 'rgba(103, 80, 164, 0.05)'} 56 | - {name: Primary Overlay 2, color: 'rgba(103, 80, 164, 0.08)'} 57 | - {name: Primary Overlay 3, color: 'rgba(103, 80, 164, 0.11)'} 58 | - name: Material Dark 59 | colors: 60 | - {name: Primary, color: '#D0BCFF'} 61 | - {name: Primary Container, color: '#4F378B'} 62 | - {name: On Primary, color: '#371E73'} 63 | - {name: On Primary Container, color: '#EADDFF'} 64 | - {name: Secondary, color: '#CCC2DC'} 65 | - {name: Secondary Container, color: '#4A4458'} 66 | - {name: On Secondary, color: '#332D41'} 67 | - {name: On Secondary Container, color: '#E8DEF8'} 68 | - {name: Tertiary, color: '#EFB8C8'} 69 | - {name: Tertiary Container, color: '#633B48'} 70 | - {name: On Tertiary, color: '#492532'} 71 | - {name: On Tertiary Container, color: '#FFD8E4'} 72 | - {name: Error, color: '#F2B8B5'} 73 | - {name: Background, color: '#1C1B1F'} 74 | - {name: Surface, color: '#1C1B1F'} 75 | - {name: On Background, color: '#E6E1E5'} 76 | - {name: On Surface, color: '#E6E1E5'} 77 | - {name: Surface Variant, color: '#49454F'} 78 | - {name: On Surface Variant, color: '#CAC4D0'} 79 | - {name: Outline, color: '#938F99'} 80 | - {name: On Disabled, color: 'rgba(230, 225, 229, 0.38)'} 81 | - {name: Disabled Container, color: 'rgba(230, 225, 229, 0.12)'} 82 | - {name: Light Overlay 1, color: 'rgba(232, 222, 248, 0.08)'} 83 | - {name: Light Overlay 2, color: 'rgba(232, 222, 248, 0.12)'} 84 | - {name: Dark Overlay 1, color: 'rgba(232, 222, 248, 0.08)'} 85 | - {name: Dark Overlay 2, color: 'rgba(232, 222, 248, 0.12)'} 86 | - {name: Primary Overlay 1, color: 'rgba(208, 188, 255, 0.05)'} 87 | - {name: Primary Overlay 2, color: 'rgba(208, 188, 255, 0.08)'} 88 | - {name: Primary Overlay 3, color: 'rgba(208, 188, 255, 0.11)'} 89 | - name: Rally Dark 90 | colors: 91 | - {name: Primary, color: '#1EB980'} 92 | - {name: Primary Container, color: '#005235'} 93 | - {name: On Primary, color: '#003824'} 94 | - {name: On Primary Container, color: '#73FBBC'} 95 | - {name: Secondary, color: '#B4CCBC'} 96 | - {name: Secondary Container, color: '#364B3F'} 97 | - {name: On Secondary, color: '#20352A'} 98 | - {name: On Secondary Container, color: '#D0E8D8'} 99 | - {name: Tertiary, color: '#A4CDDD'} 100 | - {name: Tertiary Container, color: '#234C5A'} 101 | - {name: On Tertiary, color: '#063542'} 102 | - {name: On Tertiary Container, color: '#C0E9FA'} 103 | - {name: Error, color: '#D64D47'} 104 | - {name: Background, color: '#191C1A'} 105 | - {name: Surface, color: '#191C1A'} 106 | - {name: On Background, color: '#E1E3DF'} 107 | - {name: On Surface, color: '#E1E3DF'} 108 | - {name: Surface Variant, color: '#404943'} 109 | - {name: On Surface Variant, color: '#C0C9C1'} 110 | - {name: Outline, color: '#8A938C'} 111 | - {name: Dark Overlay 1, color: 'rgba(208, 232, 216, 0.2)'} 112 | - {name: Dark Overlay 2, color: 'rgba(208, 232, 216, 0.5)'} 113 | - {name: Light Overlay 1, color: 'rgba(208, 232, 216, 0.2)'} 114 | - {name: Light Overlay 2, color: 'rgba(208, 232, 216, 0.5)'} 115 | - {name: Disabled Container, color: 'rgba(133, 133, 139, 0.12)'} 116 | - {name: On Disabled, color: '#85858B'} 117 | - {name: Primary Overlay 1, color: 'rgba(30, 185, 128, 0.05)'} 118 | - {name: Primary Overlay 2, color: 'rgba(30, 185, 128, 0.08)'} 119 | - {name: Primary Overlay 3, color: 'rgba(30, 185, 128, 0.11)'} 120 | - name: Rally Light 121 | colors: 122 | - {name: Primary, color: '#006C48'} 123 | - {name: Primary Container, color: '#00A36C'} 124 | - {name: On Primary, color: '#FFFFFF'} 125 | - {name: On Primary Container, color: '#002113'} 126 | - {name: Secondary, color: '#496455'} 127 | - {name: Secondary Container, color: '#CBEAD6'} 128 | - {name: On Secondary, color: '#FFFFFF'} 129 | - {name: On Secondary Container, color: '#052014'} 130 | - {name: Tertiary, color: '#326576'} 131 | - {name: Tertiary Container, color: '#B8EAFF'} 132 | - {name: On Tertiary, color: '#FFFFFF'} 133 | - {name: On Tertiary Container, color: '#001F28'} 134 | - {name: Error, color: '#D64D47'} 135 | - {name: Background, color: '#FBFDF8'} 136 | - {name: Surface, color: '#FBFDF8'} 137 | - {name: On Background, color: '#191C1A'} 138 | - {name: On Surface, color: '#191C1A'} 139 | - {name: Surface Variant, color: '#DCE5DD'} 140 | - {name: On Surface Variant, color: '#404943'} 141 | - {name: Outline, color: '#707973'} 142 | - {name: Dark Overlay 1, color: 'rgba(5, 32, 20, 0.8)'} 143 | - {name: Dark Overlay 2, color: 'rgba(51, 51, 61, 0.12)'} 144 | - {name: Light Overlay 1, color: 'rgba(255, 255, 255, 0.8)'} 145 | - {name: Light Overlay 2, color: 'rgba(255, 255, 255, 0.12)'} 146 | - {name: Disabled Container, color: 'rgba(25, 28, 26, 0.12)'} 147 | - {name: On Disabled, color: 'rgba(25, 28, 26, 0.38)'} 148 | - {name: Primary Overlay 1, color: 'rgba(0, 108, 72, 0.05)'} 149 | - {name: Primary Overlay 2, color: 'rgba(0, 108, 72, 0.08)'} 150 | - {name: Primary Overlay 3, color: 'rgba(0, 108, 72, 0.11)'} 151 | - name: Mykonos Light 152 | colors: 153 | - {name: Primary, color: '#006874'} 154 | - {name: Primary Container, color: '#96F0FF'} 155 | - {name: On Primary, color: '#FFFFFF'} 156 | - {name: On Primary Container, color: '#001F24'} 157 | - {name: Secondary, color: '#486367'} 158 | - {name: Secondary Container, color: '#CBE8ED'} 159 | - {name: On Secondary, color: '#FFFFFF'} 160 | - {name: On Secondary Container, color: '#031F23'} 161 | - {name: Tertiary, color: '#515E80'} 162 | - {name: Tertiary Container, color: '#DAE2FF'} 163 | - {name: On Tertiary, color: '#FFFFFF'} 164 | - {name: On Tertiary Container, color: '#0C1A39'} 165 | - {name: Error, color: '#BA1A1A'} 166 | - {name: Background, color: '#FAFDFD'} 167 | - {name: Surface, color: '#FAFDFD'} 168 | - {name: On Background, color: '#191C1D'} 169 | - {name: On Surface, color: '#191C1D'} 170 | - {name: Surface Variant, color: '#DBE4E6'} 171 | - {name: On Surface Variant, color: '#3F484A'} 172 | - {name: Outline, color: '#6F797A'} 173 | - {name: On Disabled, color: 'rgba(25, 28, 29, 0.38)'} 174 | - {name: Disabled Container, color: 'rgba(25, 28, 29, 0.12)'} 175 | - {name: Light Overlay 1, color: 'rgba(255, 255, 255, 0.08)'} 176 | - {name: Light Overlay 2, color: 'rgba(255, 255, 255, 0.12)'} 177 | - {name: Dark Overlay 1, color: 'rgba(3, 31, 35, 0.08)'} 178 | - {name: Dark Overlay 2, color: 'rgba(3, 31, 35, 0.12)'} 179 | - {name: Primary Overlay 1, color: 'rgba(0, 104, 116, 0.05)'} 180 | - {name: Primary Overlay 2, color: 'rgba(0, 104, 116, 0.08)'} 181 | - {name: Primary Overlay 3, color: 'rgba(0, 104, 116, 0.11)'} 182 | - name: Mykonos Dark 183 | colors: 184 | - {name: Primary, color: '#3CD9ED'} 185 | - {name: Primary Container, color: '#004F57'} 186 | - {name: On Primary, color: '#00363D'} 187 | - {name: On Primary Container, color: '#96F0FF'} 188 | - {name: Secondary, color: '#AFCBD0'} 189 | - {name: Secondary Container, color: '#314B4F'} 190 | - {name: On Secondary, color: '#1A3438'} 191 | - {name: On Secondary Container, color: '#CBE8ED'} 192 | - {name: Tertiary, color: '#B9C6ED'} 193 | - {name: Tertiary Container, color: '#394667'} 194 | - {name: On Tertiary, color: '#23304F'} 195 | - {name: On Tertiary Container, color: '##DAE2FF'} 196 | - {name: Error, color: '#FFB4AB'} 197 | - {name: Background, color: '#191C1D'} 198 | - {name: Surface, color: '#191C1D'} 199 | - {name: On Background, color: '#E1E3E3'} 200 | - {name: On Surface, color: '#E1E3E3'} 201 | - {name: Surface Variant, color: '#3F484A'} 202 | - {name: On Surface Variant, color: '#BFC8CA'} 203 | - {name: Outline, color: '#899294'} 204 | - {name: On Disabled, color: 'rgba(225, 227, 227, 0.38)'} 205 | - {name: Disabled Container, color: 'rgba(225, 227, 227, 0.12)'} 206 | - {name: Light Overlay 1, color: 'rgba(203, 232, 237, 0.08)'} 207 | - {name: Light Overlay 2, color: 'rgba(203, 232, 237, 0.12)'} 208 | - {name: Dark Overlay 1, color: 'rgba(203, 232, 237, 0.08)'} 209 | - {name: Dark Overlay 2, color: 'rgba(203, 232, 237, 0.12)'} 210 | - {name: Primary Overlay 1, color: 'rgba(60, 217, 237, 0.05)'} 211 | - {name: Primary Overlay 2, color: 'rgba(60, 217, 237, 0.08)'} 212 | - {name: Primary Overlay 3, color: 'rgba(60, 217, 237, 0.11)'} 213 | - name: Manarola Light 214 | colors: 215 | - {name: Primary, color: '#9A4523'} 216 | - {name: Primary Container, color: '#FFDBCF'} 217 | - {name: On Primary, color: '#FFFFFF'} 218 | - {name: On Primary Container, color: '#380D00'} 219 | - {name: Secondary, color: '#77574C'} 220 | - {name: Secondary Container, color: '#FFDBCF'} 221 | - {name: On Secondary, color: '#FFFFFF'} 222 | - {name: On Secondary Container, color: '#2C160D'} 223 | - {name: Tertiary, color: '#695E2F'} 224 | - {name: Tertiary Container, color: '#F2E2A7'} 225 | - {name: On Tertiary, color: '#FFFFFF'} 226 | - {name: On Tertiary Container, color: '#221B00'} 227 | - {name: Error, color: '#BA1A1A'} 228 | - {name: Background, color: '#FFFBFF'} 229 | - {name: Surface, color: '#FFFBFF'} 230 | - {name: On Background, color: '#201A18'} 231 | - {name: On Surface, color: '#201A18'} 232 | - {name: Surface Variant, color: '#F5DED6'} 233 | - {name: On Surface Variant, color: '#53433E'} 234 | - {name: Outline, color: '#85736D'} 235 | - {name: On Disabled, color: 'rgba(32, 26, 24, 0.38)'} 236 | - {name: Disabled Container, color: 'rgba(32, 26, 24, 0.12)'} 237 | - {name: Light Overlay 1, color: 'rgba(255, 255, 255, 0.08)'} 238 | - {name: Light Overlay 2, color: 'rgba(255, 255, 255, 0.12)'} 239 | - {name: Dark Overlay 1, color: 'rgba(44, 22, 13, 0.08)'} 240 | - {name: Dark Overlay 2, color: 'rgba(44, 22, 13, 0.12)'} 241 | - {name: Primary Overlay 1, color: 'rgba(154, 69, 35, 0.05)'} 242 | - {name: Primary Overlay 2, color: 'rgba(154, 69, 35, 0.08)'} 243 | - {name: Primary Overlay 3, color: 'rgba(154, 69, 35, 0.11)'} 244 | - name: Manarola Dark 245 | colors: 246 | - {name: Primary, color: '#FFB59B'} 247 | - {name: Primary Container, color: '#7B2E0E'} 248 | - {name: On Primary, color: '#5B1A00'} 249 | - {name: On Primary Container, color: '#FFDBCF'} 250 | - {name: Secondary, color: '#E7BDB0'} 251 | - {name: Secondary Container, color: '#5D4036'} 252 | - {name: On Secondary, color: '#442A21'} 253 | - {name: On Secondary Container, color: '#FFDBCF'} 254 | - {name: Tertiary, color: '#D5C68E'} 255 | - {name: Tertiary Container, color: '#50461A'} 256 | - {name: On Tertiary, color: '#393005'} 257 | - {name: On Tertiary Container, color: '#F2E2A7'} 258 | - {name: Error, color: '#FFB4AB'} 259 | - {name: Background, color: '#201A18'} 260 | - {name: Surface, color: '#201A18'} 261 | - {name: On Background, color: '#EDE0DC'} 262 | - {name: On Surface, color: '#EDE0DC'} 263 | - {name: Surface Variant, color: '#53433E'} 264 | - {name: On Surface Variant, color: '#D8C2BB'} 265 | - {name: Outline, color: '#A08D86'} 266 | - {name: On Disabled, color: 'rgba(237, 224, 220, 0.38)'} 267 | - {name: Disabled Container, color: 'rgba(237, 224, 220, 0.12)'} 268 | - {name: Light Overlay 1, color: 'rgba(255, 219, 207, 0.08)'} 269 | - {name: Light Overlay 2, color: 'rgba(255, 219, 207, 0.12)'} 270 | - {name: Dark Overlay 1, color: 'rgba(255, 219, 207, 0.08)'} 271 | - {name: Dark Overlay 2, color: 'rgba(255, 219, 207, 0.12)'} 272 | - {name: Primary Overlay 1, color: 'rgba(255, 181, 155, 0.05)'} 273 | - {name: Primary Overlay 2, color: 'rgba(255, 181, 155, 0.08)'} 274 | - {name: Primary Overlay 3, color: 'rgba(255, 181, 155, 0.11)'} 275 | colors: 276 | - {name: Primary, color: '#1EB980'} 277 | - {name: Primary Container, color: '#005235'} 278 | - {name: On Primary, color: '#003824'} 279 | - {name: On Primary Container, color: '#73FBBC'} 280 | - {name: Secondary, color: '#B4CCBC'} 281 | - {name: Secondary Container, color: '#364B3F'} 282 | - {name: On Secondary, color: '#20352A'} 283 | - {name: On Secondary Container, color: '#D0E8D8'} 284 | - {name: Tertiary, color: '#A4CDDD'} 285 | - {name: Tertiary Container, color: '#234C5A'} 286 | - {name: On Tertiary, color: '#063542'} 287 | - {name: On Tertiary Container, color: '#C0E9FA'} 288 | - {name: Error, color: '#D64D47'} 289 | - {name: Background, color: '#191C1A'} 290 | - {name: Surface, color: '#191C1A'} 291 | - {name: On Background, color: '#E1E3DF'} 292 | - {name: On Surface, color: '#E1E3DF'} 293 | - {name: Surface Variant, color: '#404943'} 294 | - {name: On Surface Variant, color: '#C0C9C1'} 295 | - {name: Outline, color: '#8A938C'} 296 | - {name: Dark Overlay 1, color: 'rgba(208, 232, 216, 0.2)'} 297 | - {name: Dark Overlay 2, color: 'rgba(208, 232, 216, 0.5)'} 298 | - {name: Light Overlay 1, color: 'rgba(208, 232, 216, 0.2)'} 299 | - {name: Light Overlay 2, color: 'rgba(208, 232, 216, 0.5)'} 300 | - {name: Disabled Container, color: 'rgba(133, 133, 139, 0.12)'} 301 | - {name: On Disabled, color: '#85858B'} 302 | - {name: Primary Overlay 1, color: 'rgba(30, 185, 128, 0.05)'} 303 | - {name: Primary Overlay 2, color: 'rgba(30, 185, 128, 0.08)'} 304 | - {name: Primary Overlay 3, color: 'rgba(30, 185, 128, 0.11)'} 305 | -------------------------------------------------------------------------------- /theme/templates.yaml: -------------------------------------------------------------------------------- 1 | - name: Standard Page 2 | description: A page with an a left-side navigation panel. 3 | img: /img/form-templates/rally.png 4 | form: 5 | class_name: Form 6 | is_package: true 7 | container: 8 | type: HtmlTemplate 9 | properties: {html: '@theme:standard-page.html'} 10 | components: 11 | - type: ColumnPanel 12 | properties: {} 13 | name: content_panel 14 | layout_properties: {slot: default} 15 | code: "from ._anvil_designer import $NAME$Template\nfrom anvil import *\n\nclass\ 16 | \ $NAME$($NAME$Template):\n\n def __init__(self, **properties):\n # Set\ 17 | \ Form properties and Data Bindings.\n self.init_components(**properties)\n\ 18 | \n # Any code you write here will run before the form opens.\n \n" 19 | --------------------------------------------------------------------------------